From d973e110616c132f1d8df676e7039a57b9134537 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 8 Jan 2019 18:33:43 +0100 Subject: Add UI option to show local-only toots in public timeline --- app/javascript/flavours/glitch/actions/compose.js | 4 ++- .../flavours/glitch/actions/streaming.js | 2 +- .../flavours/glitch/actions/timelines.js | 2 +- .../public_timeline/components/column_settings.js | 1 + .../glitch/features/public_timeline/index.js | 29 ++++++++++++---------- 5 files changed, 22 insertions(+), 16 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f98cb7bf8..a6363accb 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -193,7 +193,9 @@ export function submitCompose(routerHistory) { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { insertIfOnline('community'); - insertIfOnline('public'); + if (!response.data.local_only) { + insertIfOnline('public'); + } } else if (response.data.visibility === 'direct') { insertIfOnline('direct'); } diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index e7e57f5f5..0253c24b2 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); -export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`); +export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`); export const connectHashtagStream = (id, tag, local, accept) => connectTimelineStream(`hashtag:${id}${local ? ':local' : ''}`, `hashtag${local ? ':local' : ''}&tag=${tag}`, null, accept); export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 9a7f62a08..b19666e62 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -130,7 +130,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); -export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); diff --git a/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.js index 756b6fe06..e92681065 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.js @@ -22,6 +22,7 @@ class ColumnSettings extends React.PureComponent {
} /> } /> + {!settings.getIn(['other', 'onlyRemote']) && } />}
); diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index 3f720b885..848049965 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -20,12 +20,14 @@ const mapStateToProps = (state, { columnId }) => { const index = columns.findIndex(c => c.get('uuid') === uuid); const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']); + const allowLocalOnly = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'allowLocalOnly']) : state.getIn(['settings', 'public', 'other', 'allowLocalOnly']); const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); return { hasUnread: !!timelineState && timelineState.get('unread') > 0, onlyMedia, onlyRemote, + allowLocalOnly, }; }; @@ -49,15 +51,16 @@ class PublicTimeline extends React.PureComponent { hasUnread: PropTypes.bool, onlyMedia: PropTypes.bool, onlyRemote: PropTypes.bool, + allowLocalOnly: PropTypes.bool, }; handlePin = () => { - const { columnId, dispatch, onlyMedia, onlyRemote } = this.props; + const { columnId, dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; if (columnId) { dispatch(removeColumn(columnId)); } else { - dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } })); + dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote, allowLocalOnly } })); } } @@ -71,19 +74,19 @@ class PublicTimeline extends React.PureComponent { } componentDidMount () { - const { dispatch, onlyMedia, onlyRemote } = this.props; + const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; - dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote })); + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly })); } componentDidUpdate (prevProps) { - if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) { - const { dispatch, onlyMedia, onlyRemote } = this.props; + if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote || prevProps.allowLocalOnly !== this.props.allowLocalOnly) { + const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; this.disconnect(); - dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote })); + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote, allowLocalOnly })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote, allowLocalOnly })); } } @@ -99,13 +102,13 @@ class PublicTimeline extends React.PureComponent { } handleLoadMore = maxId => { - const { dispatch, onlyMedia, onlyRemote } = this.props; + const { dispatch, onlyMedia, onlyRemote, allowLocalOnly } = this.props; - dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote })); + dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote, allowLocalOnly })); } render () { - const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props; + const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote, allowLocalOnly } = this.props; const pinned = !!columnId; return ( @@ -124,7 +127,7 @@ class PublicTimeline extends React.PureComponent { Date: Mon, 6 Jul 2020 19:27:32 +0700 Subject: [Glitch] Replace shortNumberFormat with Port cb2adaaf9d6c3147de9060132b69933df734d5dc to glitch-soc Signed-off-by: Thibaut Girka --- .../glitch/components/autosuggest_hashtag.js | 24 ++- .../flavours/glitch/components/common_counter.js | 62 ++++++++ .../flavours/glitch/components/hashtag.js | 49 +++++- .../flavours/glitch/components/short_number.js | 117 ++++++++++++++ .../glitch/features/account/components/header.js | 1 - .../features/directory/components/account_card.js | 171 ++++++++++++++++----- app/javascript/flavours/glitch/util/numbers.js | 85 ++++++++-- 7 files changed, 445 insertions(+), 64 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/common_counter.js create mode 100644 app/javascript/flavours/glitch/components/short_number.js (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js index 648987dfd..d787ed07a 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import ShortNumber from 'flavours/glitch/components/short_number'; import { FormattedMessage } from 'react-intl'; export default class AutosuggestHashtag extends React.PureComponent { @@ -13,14 +13,28 @@ export default class AutosuggestHashtag extends React.PureComponent { }).isRequired, }; - render () { + render() { const { tag } = this.props; - const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0)); + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); return (
-
#{tag.name}
- {tag.history !== undefined &&
} +
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )}
); } diff --git a/app/javascript/flavours/glitch/components/common_counter.js b/app/javascript/flavours/glitch/components/common_counter.js new file mode 100644 index 000000000..4fdf3babf --- /dev/null +++ b/app/javascript/flavours/glitch/components/common_counter.js @@ -0,0 +1,62 @@ +// @ts-check +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +/** + * Returns custom renderer for one of the common counter types + * + * @param {"statuses" | "following" | "followers"} counterType + * Type of the counter + * @param {boolean} isBold Whether display number must be displayed in bold + * @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} + * Renderer function + * @throws If counterType is not covered by this function + */ +export function counterRenderer(counterType, isBold = true) { + /** + * @type {(displayNumber: JSX.Element) => JSX.Element} + */ + const renderCounter = isBold + ? (displayNumber) => {displayNumber} + : (displayNumber) => displayNumber; + + switch (counterType) { + case 'statuses': { + return (displayNumber, pluralReady) => ( + + ); + } + case 'following': { + return (displayNumber, pluralReady) => ( + + ); + } + case 'followers': { + return (displayNumber, pluralReady) => ( + + ); + } + default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`); + } +} diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index d42bee0e9..639d87a1e 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -1,26 +1,65 @@ +// @ts-check import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; -import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import ShortNumber from 'flavours/glitch/components/short_number'; + +/** + * Used to render counter of how much people are talking about hashtag + * + * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} + */ +const accountsCountRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); const Hashtag = ({ hashtag }) => (
- + #{hashtag.get('name')} - {shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)} }} /> +
- {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} +
- day.get('uses')).toArray()}> + day.get('uses')) + .toArray()} + >
diff --git a/app/javascript/flavours/glitch/components/short_number.js b/app/javascript/flavours/glitch/components/short_number.js new file mode 100644 index 000000000..e4ba09634 --- /dev/null +++ b/app/javascript/flavours/glitch/components/short_number.js @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../util/numbers'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; +// @ts-check + +/** + * @callback ShortNumberRenderer + * @param {JSX.Element} displayNumber Number to display + * @param {number} pluralReady Number used for pluralization + * @returns {JSX.Element} Final render of number + */ + +/** + * @typedef {object} ShortNumberProps + * @property {number} value Number to display in short variant + * @property {ShortNumberRenderer} [renderer] + * Custom renderer for numbers, provided as a prop. If another renderer + * passed as a child of this component, this prop won't be used. + * @property {ShortNumberRenderer} [children] + * Custom renderer for numbers, provided as a child. If another renderer + * passed as a prop of this component, this one will be used instead. + */ + +/** + * Component that renders short big number to a shorter version + * + * @param {ShortNumberProps} param0 Props for the component + * @returns {JSX.Element} Rendered number + */ +function ShortNumber({ value, renderer, children }) { + const shortNumber = toShortNumber(value); + const [, division] = shortNumber; + + // eslint-disable-next-line eqeqeq + if (children != null && renderer != null) { + console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); + } + + // eslint-disable-next-line eqeqeq + const customRenderer = children != null ? children : renderer; + + const displayNumber = ; + + // eslint-disable-next-line eqeqeq + return customRenderer != null + ? customRenderer(displayNumber, pluralReady(value, division)) + : displayNumber; +} + +ShortNumber.propTypes = { + value: PropTypes.number.isRequired, + renderer: PropTypes.func, + children: PropTypes.func, +}; + +/** + * @typedef {object} ShortNumberCounterProps + * @property {import('../util/number').ShortNumber} value Short number + */ + +/** + * Renders short number into corresponding localizable react fragment + * + * @param {ShortNumberCounterProps} param0 Props for the component + * @returns {JSX.Element} FormattedMessage ready to be embedded in code + */ +function ShortNumberCounter({ value }) { + const [rawNumber, unit, maxFractionDigits = 0] = value; + + const count = ( + + ); + + let values = { count, rawNumber }; + + switch (unit) { + case DECIMAL_UNITS.THOUSAND: { + return ( + + ); + } + case DECIMAL_UNITS.MILLION: { + return ( + + ); + } + case DECIMAL_UNITS.BILLION: { + return ( + + ); + } + // Not sure if we should go farther - @Sasha-Sorokin + default: return count; + } +} + +ShortNumberCounter.propTypes = { + value: PropTypes.arrayOf(PropTypes.number), +}; + +export default React.memo(ShortNumber); diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index a5abf38ae..4ef8036fc 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -9,7 +9,6 @@ import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import Avatar from 'flavours/glitch/components/avatar'; import Button from 'flavours/glitch/components/button'; -import { shortNumberFormat } from 'flavours/glitch/util/numbers'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index 557120960..2ef9d5ba4 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -11,8 +11,14 @@ import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import IconButton from 'flavours/glitch/components/icon_button'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state'; -import { shortNumberFormat } from 'flavours/glitch/util/numbers'; -import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'flavours/glitch/actions/accounts'; +import ShortNumber from 'flavours/glitch/components/short_number'; +import { + followAccount, + unfollowAccount, + blockAccount, + unblockAccount, + unmuteAccount, +} from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; @@ -22,7 +28,10 @@ const messages = defineMessages({ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, + unfollowConfirm: { + id: 'confirmations.unfollow.confirm', + defaultMessage: 'Unfollow', + }, }); const makeMapStateToProps = () => { @@ -36,15 +45,25 @@ const makeMapStateToProps = () => { }; const mapDispatchToProps = (dispatch, { intl }) => ({ - - onFollow (account) { - if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { + onFollow(account) { + if ( + account.getIn(['relationship', 'following']) || + account.getIn(['relationship', 'requested']) + ) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - })); + dispatch( + openModal('CONFIRM', { + message: ( + @{account.get('acct')} }} + /> + ), + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }), + ); } else { dispatch(unfollowAccount(account.get('id'))); } @@ -53,7 +72,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onBlock (account) { + onBlock(account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); } else { @@ -61,17 +80,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onMute (account) { + onMute(account) { if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); } else { dispatch(initMuteModal(account)); } }, - }); -export default @injectIntl +export default +@injectIntl @connect(makeMapStateToProps, mapDispatchToProps) class AccountCard extends ImmutablePureComponent { @@ -83,7 +102,7 @@ class AccountCard extends ImmutablePureComponent { onMute: PropTypes.func.isRequired, }; - _updateEmojis () { + _updateEmojis() { const node = this.node; if (!node || autoPlayGif) { @@ -104,68 +123,113 @@ class AccountCard extends ImmutablePureComponent { } } - componentDidMount () { + componentDidMount() { this._updateEmojis(); } - componentDidUpdate () { + componentDidUpdate() { this._updateEmojis(); } handleEmojiMouseEnter = ({ target }) => { target.src = target.getAttribute('data-original'); - } + }; handleEmojiMouseLeave = ({ target }) => { target.src = target.getAttribute('data-static'); - } + }; handleFollow = () => { this.props.onFollow(this.props.account); - } + }; handleBlock = () => { this.props.onBlock(this.props.account); - } + }; handleMute = () => { this.props.onMute(this.props.account); - } + }; setRef = (c) => { this.node = c; - } + }; - render () { + render() { const { account, intl } = this.props; let buttons; - if (account.get('id') !== me && account.get('relationship', null) !== null) { + if ( + account.get('id') !== me && + account.get('relationship', null) !== null + ) { const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); if (requested) { - buttons = ; + buttons = ( + + ); } else if (blocking) { - buttons = ; + buttons = ( + + ); } else if (muting) { - buttons = ; + buttons = ( + + ); } else if (!account.get('moved') || following) { - buttons = ; + buttons = ( + + ); } } return (
- +
- + @@ -176,13 +240,44 @@ class AccountCard extends ImmutablePureComponent {
-
+
-
{shortNumberFormat(account.get('statuses_count'))}
-
{account.get('followers_count') < 0 ? '-' : shortNumberFormat(account.get('followers_count'))}
-
{account.get('last_status_at') === null ? : }
+
+ + + + +
+
+ {account.get('followers_count') < 0 ? '-' : }{' '} + + + +
+
+ {account.get('last_status_at') === null ? ( + + ) : ( + + )}{' '} + + + +
); diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js index af18dcfdd..6f2505cae 100644 --- a/app/javascript/flavours/glitch/util/numbers.js +++ b/app/javascript/flavours/glitch/util/numbers.js @@ -1,16 +1,71 @@ -import React, { Fragment } from 'react'; -import { FormattedNumber } from 'react-intl'; - -export const shortNumberFormat = number => { - if (number < 1000) { - return ; - } else if (number < 10000) { - return K; - } else if (number < 1000000) { - return K; - } else if (number < 10000000) { - return M; - } else { - return M; +// @ts-check + +export const DECIMAL_UNITS = Object.freeze({ + ONE: 1, + TEN: 10, + HUNDRED: Math.pow(10, 2), + THOUSAND: Math.pow(10, 3), + MILLION: Math.pow(10, 6), + BILLION: Math.pow(10, 9), + TRILLION: Math.pow(10, 12), +}); + +const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10; +const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10; + +/** + * @typedef {[number, number, number]} ShortNumber + * Array of: shorten number, unit of shorten number and maximum fraction digits + */ + +/** + * @param {number} sourceNumber Number to convert to short number + * @returns {ShortNumber} Calculated short number + * @example + * shortNumber(5936); + * // => [5.936, 1000, 1] + */ +export function toShortNumber(sourceNumber) { + if (sourceNumber < DECIMAL_UNITS.THOUSAND) { + return [sourceNumber, DECIMAL_UNITS.ONE, 0]; + } else if (sourceNumber < DECIMAL_UNITS.MILLION) { + return [ + sourceNumber / DECIMAL_UNITS.THOUSAND, + DECIMAL_UNITS.THOUSAND, + sourceNumber < TEN_THOUSAND ? 1 : 0, + ]; + } else if (sourceNumber < DECIMAL_UNITS.BILLION) { + return [ + sourceNumber / DECIMAL_UNITS.MILLION, + DECIMAL_UNITS.MILLION, + sourceNumber < TEN_MILLIONS ? 1 : 0, + ]; + } else if (sourceNumber < DECIMAL_UNITS.TRILLION) { + return [ + sourceNumber / DECIMAL_UNITS.BILLION, + DECIMAL_UNITS.BILLION, + 0, + ]; } -}; + + return [sourceNumber, DECIMAL_UNITS.ONE, 0]; +} + +/** + * @param {number} sourceNumber Original number that is shortened + * @param {number} division The scale in which short number is displayed + * @returns {number} Number that can be used for plurals when short form used + * @example + * pluralReady(1793, DECIMAL_UNITS.THOUSAND) + * // => 1790 + */ +export function pluralReady(sourceNumber, division) { + // eslint-disable-next-line eqeqeq + if (division == null || division < DECIMAL_UNITS.HUNDRED) { + return sourceNumber; + } + + let closestScale = division / DECIMAL_UNITS.TEN; + + return Math.trunc(sourceNumber / closestScale) * closestScale; +} -- cgit From 170b38c3f44ba01a9896a1c5392f6b8cab5998c9 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 7 Jul 2020 17:24:23 +0200 Subject: Fix being unable to add account notes --- app/javascript/flavours/glitch/features/account/components/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 4ef8036fc..572f34fa0 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -176,7 +176,7 @@ class Header extends ImmutablePureComponent { menu.push(null); } - if (accountNote === null) { + if (accountNote === null || accountNote === '') { menu.push({ text: intl.formatMessage(messages.add_account_note, { name: account.get('username') }), action: this.props.onEditAccountNote }); } -- cgit From a9b13804e25b1697fcbf2dcda5835a0dfdc5cd50 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 7 Jul 2020 17:42:35 +0200 Subject: Change account note placeholder name since the design has diverged upstream --- .../flavours/glitch/features/account/components/account_note.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.js b/app/javascript/flavours/glitch/features/account/components/account_note.js index e7fd4c5ff..3163b8735 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.js +++ b/app/javascript/flavours/glitch/features/account/components/account_note.js @@ -7,7 +7,7 @@ import Icon from 'flavours/glitch/components/icon'; import Textarea from 'react-textarea-autosize'; const messages = defineMessages({ - placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' }, + placeholder: { id: 'account_note.glitch_placeholder', defaultMessage: 'No comment provided' }, }); export default @injectIntl -- cgit From 64b6c20676f320686c58e321b674cba757e40905 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 7 Jul 2020 17:47:33 +0200 Subject: Change styling and layout of account notes UI --- .../features/account/components/account_note.js | 19 +++++++------ .../glitch/styles/components/accounts.scss | 32 ++++++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.js b/app/javascript/flavours/glitch/features/account/components/account_note.js index 3163b8735..8a99bfcb8 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.js +++ b/app/javascript/flavours/glitch/features/account/components/account_note.js @@ -63,6 +63,14 @@ class Header extends ImmutablePureComponent {
); + } else { + action_buttons = ( +
+ +
+ ); } let note_container = null; @@ -85,17 +93,10 @@ class Header extends ImmutablePureComponent { return (
- - {!isEditing && ( -
- -
- )} + + {action_buttons}
{note_container} - {action_buttons}
); } diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 774254a4c..c1e3ae835 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -714,42 +714,44 @@ } &__account-note { - margin: 5px; - padding: 10px; - background: $ui-highlight-color; - color: $primary-text-color; + margin: 0 -5px; + padding: 10px 15px; display: flex; flex-direction: column; - border-radius: 4px; font-size: 14px; font-weight: 400; + border-top: 1px solid lighten($ui-base-color, 12%); + border-bottom: 1px solid lighten($ui-base-color, 12%); &__header { display: flex; flex-direction: row; justify-content: space-between; + margin-bottom: 5px; + color: $darker-text-color; } &__content { white-space: pre-wrap; - margin-top: 5px; + padding: 10px 0; } &__buttons { display: flex; flex-direction: row; justify-content: flex-end; - margin-top: 5px; + flex: 1 0; .flex-spacer { - flex: 0 0 20px; + flex: 0 0 15px; background: transparent; } } strong { - font-size: 15px; + font-size: 12px; font-weight: 500; + text-transform: uppercase; } button:hover span { @@ -759,18 +761,24 @@ textarea { display: block; box-sizing: border-box; - width: 100%; + width: calc(100% + 20px); margin: 0; margin-top: 5px; - color: $inverted-text-color; - background: $simple-background-color; + color: $secondary-text-color; + background: $ui-base-color; padding: 10px; + margin: 0 -10px; font-family: inherit; font-size: 14px; resize: none; border: 0; outline: 0; border-radius: 4px; + + &::placeholder { + color: $dark-text-color; + opacity: 1; + } } } } -- cgit From a513997367b050028f3ad6e9a0bff46346b5b832 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 8 Jul 2020 09:22:23 +0200 Subject: [Glitch] Fix WebUI crash on sensitive preview card with no preview thumbnail Port 258171549120142a6a7dac40f17ecc2087433f4a to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/status/components/card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index 4b6676062..b4db62f4a 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -98,7 +98,7 @@ export default class Card extends React.PureComponent { componentDidUpdate (prevProps) { const { card } = this.props; - if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash'))) { + if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash')) && this.canvas) { this._decode(); } } -- cgit From b27eecdbfcebc46c8e29c6b834b9c8b25ddfa751 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 8 Jul 2020 12:58:17 +0200 Subject: [Glitch] Fix WebUI crash on sensitive preview card with no preview thumbnail Port d308a863fbd373b94fa571103ad431782c29e074 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/status/components/card.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index b4db62f4a..ab6398e1a 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -86,7 +86,7 @@ export default class Card extends React.PureComponent { componentDidMount () { window.addEventListener('resize', this.handleResize, { passive: true }); - if (this.props.card && this.props.card.get('blurhash')) { + if (this.props.card && this.props.card.get('blurhash') && this.canvas) { this._decode(); } } -- cgit From 11446be6d13f6d4748227a329dcc75c5c78c915d Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 8 Jul 2020 14:54:47 +0200 Subject: [Glitch] Fix new accent color not refreshing when changing thumbnail for audio uploads Port 0d2135a46172fd6931f757ef083ad99f4522081d to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/features/audio/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js index 181d8e980..d833e0fe9 100644 --- a/app/javascript/flavours/glitch/features/audio/index.js +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -102,7 +102,7 @@ class Audio extends React.PureComponent { } componentDidUpdate (prevProps, prevState) { - if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height) { + if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height || prevProps.accentColor !== this.props.accentColor) { this._clear(); this._draw(); } -- cgit From 0fe5deae89f140d2721e5c914b4ad06fea426623 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 7 Jul 2020 19:26:08 +0200 Subject: Change styling of account note editing buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Mélanie Chauvel (ariasuni) --- .../features/account/components/account_note.js | 6 ++--- .../glitch/styles/components/accounts.scss | 29 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.js b/app/javascript/flavours/glitch/features/account/components/account_note.js index 8a99bfcb8..6e5ed7703 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.js +++ b/app/javascript/flavours/glitch/features/account/components/account_note.js @@ -54,11 +54,11 @@ class Header extends ImmutablePureComponent { if (isEditing) { action_buttons = (
-
-
@@ -66,7 +66,7 @@ class Header extends ImmutablePureComponent { } else { action_buttons = (
-
diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index c1e3ae835..0fc2a11ff 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -742,8 +742,31 @@ justify-content: flex-end; flex: 1 0; + .icon-button { + font-size: 14px; + padding: 2px 6px; + color: $darker-text-color; + + &:hover, + &:active, + &:focus { + color: lighten($darker-text-color, 7%); + background-color: rgba($darker-text-color, 0.15); + } + + &:focus { + background-color: rgba($darker-text-color, 0.3); + } + + &[disabled] { + color: darken($darker-text-color, 13%); + background-color: transparent; + cursor: default; + } + } + .flex-spacer { - flex: 0 0 15px; + flex: 0 0 5px; background: transparent; } } @@ -754,10 +777,6 @@ text-transform: uppercase; } - button:hover span { - text-decoration: underline; - } - textarea { display: block; box-sizing: border-box; -- cgit From d42a23fdbe59f5a8f2beed4201ccd6d797fbd5f4 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 10 Jul 2020 09:50:41 +0200 Subject: Fix clicking the audio player also opening toots in detailed view --- app/javascript/flavours/glitch/components/status_content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index a5822866a..a39f747b8 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -231,7 +231,7 @@ export default class StatusContent extends React.PureComponent { let element = e.target; while (element) { - if (['button', 'video', 'a', 'label', 'wave'].includes(element.localName)) { + if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) { return; } element = element.parentNode; -- cgit From 042c32ea3bcb4f5986dcb6b39a25a8cead3bf86a Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Thu, 9 Jul 2020 18:01:30 +0700 Subject: [Glitch] Replace repetitive blurhash code with component (#14267) Port 61c07c37317f01c1ab4981826704750fe9937fe7 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/components/blurhash.js | 61 ++++++++++++++++++++++ .../flavours/glitch/components/media_gallery.js | 46 +++++----------- .../account_gallery/components/media_item.js | 40 ++++---------- .../glitch/features/status/components/card.js | 42 ++++----------- .../flavours/glitch/features/video/index.js | 41 ++++----------- 5 files changed, 102 insertions(+), 128 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/blurhash.js (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/blurhash.js b/app/javascript/flavours/glitch/components/blurhash.js new file mode 100644 index 000000000..172f8c2f5 --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.js @@ -0,0 +1,61 @@ +// @ts-check + +import { decode } from 'blurhash'; +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +/** + * @typedef BlurhashPropsBase + * @property {string} hash Hash to render + * @property {number} width + * Width of the blurred region in pixels. Defaults to 32 + * @property {number} [height] + * Height of the blurred region in pixels. Defaults to width + * @property {boolean} [dummy] + * Whether dummy mode is enabled. If enabled, nothing is rendered + * and canvas left untouched + */ + +/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */ + +/** + * Component that is used to render blurred of blurhash string + * + * @param {BlurhashProps} param1 Props of the component + * @returns Canvas which will render blurred region element to embed + */ +function Blurhash({ + hash, + width = 32, + height = width, + dummy = false, + ...canvasProps +}) { + const canvasRef = /** @type {import('react').MutableRefObject} */ (useRef()); + + useEffect(() => { + const { current: canvas } = canvasRef; + canvas.width = canvas.width; // resets canvas + + if (dummy) return; + + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + ctx.putImageData(imageData, 0, 0); + }, [dummy, hash, width, height]); + + return ( + + ); +} + +Blurhash.propTypes = { + hash: PropTypes.string.isRequired, + width: PropTypes.number, + height: PropTypes.number, + dummy: PropTypes.bool, +}; + +export default React.memo(Blurhash); diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 71240530c..3a4839414 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -7,8 +7,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from 'flavours/glitch/util/is_mobile'; import classNames from 'classnames'; import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; -import { decode } from 'blurhash'; import { debounce } from 'lodash'; +import Blurhash from 'flavours/glitch/components/blurhash'; const messages = defineMessages({ hidden: { @@ -94,36 +94,6 @@ class Item extends React.PureComponent { e.stopPropagation(); } - componentDidMount () { - if (this.props.attachment.get('blurhash')) { - this._decode(); - } - } - - componentDidUpdate (prevProps) { - if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) { - this._decode(); - } - } - - _decode () { - if (!useBlurhash) return; - - const hash = this.props.attachment.get('blurhash'); - const pixels = decode(hash, 32, 32); - - if (pixels) { - const ctx = this.canvas.getContext('2d'); - const imageData = new ImageData(pixels, 32, 32); - - ctx.putImageData(imageData, 0, 0); - } - } - - setCanvasRef = c => { - this.canvas = c; - } - handleImageLoad = () => { this.setState({ loaded: true }); } @@ -186,7 +156,11 @@ class Item extends React.PureComponent { return ( ); @@ -253,7 +227,13 @@ class Item extends React.PureComponent { return (
- + {visible && thumbnail}
); diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index f1cb3f9e4..b88f23aa4 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -1,7 +1,7 @@ -import { decode } from 'blurhash'; +import Blurhash from 'flavours/glitch/components/blurhash'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; import { isIOS } from 'flavours/glitch/util/is_mobile'; import PropTypes from 'prop-types'; import React from 'react'; @@ -21,34 +21,6 @@ export default class MediaItem extends ImmutablePureComponent { loaded: false, }; - componentDidMount () { - if (this.props.attachment.get('blurhash')) { - this._decode(); - } - } - - componentDidUpdate (prevProps) { - if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) { - this._decode(); - } - } - - _decode () { - const hash = this.props.attachment.get('blurhash'); - const pixels = decode(hash, 32, 32); - - if (pixels) { - const ctx = this.canvas.getContext('2d'); - const imageData = new ImageData(pixels, 32, 32); - - ctx.putImageData(imageData, 0, 0); - } - } - - setCanvasRef = c => { - this.canvas = c; - } - handleImageLoad = () => { this.setState({ loaded: true }); } @@ -149,7 +121,13 @@ export default class MediaItem extends ImmutablePureComponent { return ( diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index ab6398e1a..14abe9838 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; import Icon from 'flavours/glitch/components/icon'; import { useBlurhash } from 'flavours/glitch/util/initial_state'; -import { decode } from 'blurhash'; +import Blurhash from 'flavours/glitch/components/blurhash'; import { debounce } from 'lodash'; const getHostname = url => { @@ -85,38 +85,12 @@ export default class Card extends React.PureComponent { componentDidMount () { window.addEventListener('resize', this.handleResize, { passive: true }); - - if (this.props.card && this.props.card.get('blurhash') && this.canvas) { - this._decode(); - } } componentWillUnmount () { window.removeEventListener('resize', this.handleResize); } - componentDidUpdate (prevProps) { - const { card } = this.props; - - if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash')) && this.canvas) { - this._decode(); - } - } - - _decode () { - if (!useBlurhash) return; - - const hash = this.props.card.get('blurhash'); - const pixels = decode(hash, 32, 32); - - if (pixels) { - const ctx = this.canvas.getContext('2d'); - const imageData = new ImageData(pixels, 32, 32); - - ctx.putImageData(imageData, 0, 0); - } - } - _setDimensions () { const width = this.node.offsetWidth; @@ -174,10 +148,6 @@ export default class Card extends React.PureComponent { } } - setCanvasRef = c => { - this.canvas = c; - } - handleImageLoad = () => { this.setState({ previewLoaded: true }); } @@ -230,7 +200,15 @@ export default class Card extends React.PureComponent { ); let embed = ''; - let canvas = ; + let canvas = ( + + ); let thumbnail = ; let spoilerButton = (
diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js index 4fa76fd6d..c2aff1b57 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversations_list.js @@ -10,6 +10,7 @@ export default class ConversationsList extends ImmutablePureComponent { static propTypes = { conversations: ImmutablePropTypes.list.isRequired, + scrollKey: PropTypes.string.isRequired, hasMore: PropTypes.bool, isLoading: PropTypes.bool, onLoadMore: PropTypes.func, @@ -57,13 +58,14 @@ export default class ConversationsList extends ImmutablePureComponent { const { conversations, onLoadMore, ...other } = this.props; return ( - + {conversations.map(item => ( ))} diff --git a/app/javascript/flavours/glitch/reducers/dropdown_menu.js b/app/javascript/flavours/glitch/reducers/dropdown_menu.js index 36fd4f132..a78a11acc 100644 --- a/app/javascript/flavours/glitch/reducers/dropdown_menu.js +++ b/app/javascript/flavours/glitch/reducers/dropdown_menu.js @@ -4,14 +4,14 @@ import { DROPDOWN_MENU_CLOSE, } from '../actions/dropdown_menu'; -const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false }); +const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false, scroll_key: null }); export default function dropdownMenu(state = initialState, action) { switch (action.type) { case DROPDOWN_MENU_OPEN: - return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard }); + return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard, scroll_key: action.scroll_key }); case DROPDOWN_MENU_CLOSE: - return state.get('openId') === action.id ? state.set('openId', null) : state; + return state.get('openId') === action.id ? state.set('openId', null).set('scroll_key', null) : state; default: return state; } -- cgit From 052027357321768bf39b2a62cb3f585b5d08b64e Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 10 Jul 2020 13:57:05 +0200 Subject: [Glitch] Audio player visualization improvements Port a2abe35e0f55c96e8b8525ee25089751859d14c2 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/features/audio/index.js | 158 ++------------------- .../flavours/glitch/features/audio/visualizer.js | 136 ++++++++++++++++++ 2 files changed, 148 insertions(+), 146 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/audio/visualizer.js (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js index d833e0fe9..120a5ce1a 100644 --- a/app/javascript/flavours/glitch/features/audio/index.js +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -7,11 +7,7 @@ import classNames from 'classnames'; import { throttle } from 'lodash'; import { getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; import { debounce } from 'lodash'; - -const hex2rgba = (hex, alpha = 1) => { - const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -}; +import Visualizer from './visualizer'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -54,6 +50,11 @@ class Audio extends React.PureComponent { dragging: false, }; + constructor (props) { + super(props); + this.visualizer = new Visualizer(TICK_SIZE); + } + setPlayerRef = c => { this.player = c; @@ -92,9 +93,7 @@ class Audio extends React.PureComponent { setCanvasRef = c => { this.canvas = c; - if (c) { - this.canvasContext = c.getContext('2d'); - } + this.visualizer.setCanvas(c); } componentDidMount () { @@ -247,17 +246,12 @@ class Audio extends React.PureComponent { _initAudioContext () { const context = new AudioContext(); - const analyser = context.createAnalyser(); const source = context.createMediaElementSource(this.audio); - analyser.smoothingTimeConstant = 0.6; - analyser.fftSize = 2048; - - source.connect(analyser); + this.visualizer.setAudioContext(context, source); source.connect(context.destination); this.audioContext = context; - this.analyser = analyser; } handleDownload = () => { @@ -290,20 +284,12 @@ class Audio extends React.PureComponent { }); } - _clear () { - this.canvasContext.clearRect(0, 0, this.state.width, this.state.height); + _clear() { + this.visualizer.clear(this.state.width, this.state.height); } - _draw () { - this.canvasContext.save(); - - const ticks = this._getTicks(360 * this._getScaleCoefficient(), TICK_SIZE); - - ticks.forEach(tick => { - this._drawTick(tick.x1, tick.y1, tick.x2, tick.y2); - }); - - this.canvasContext.restore(); + _draw() { + this.visualizer.draw(this._getCX(), this._getCY(), this._getAccentColor(), this._getRadius(), this._getScaleCoefficient()); } _getRadius () { @@ -314,126 +300,6 @@ class Audio extends React.PureComponent { return (this.state.height || this.props.height) / 982; } - _getTicks (count, size, animationParams = [0, 90]) { - const radius = this._getRadius(); - const ticks = this._getTickPoints(count); - const lesser = 200; - const m = []; - const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0; - const frequencyData = new Uint8Array(bufferLength); - const allScales = []; - const scaleCoefficient = this._getScaleCoefficient(); - - if (this.analyser) { - this.analyser.getByteFrequencyData(frequencyData); - } - - ticks.forEach((tick, i) => { - const coef = 1 - i / (ticks.length * 2.5); - - let delta = ((frequencyData[i] || 0) - lesser * coef) * scaleCoefficient; - - if (delta < 0) { - delta = 0; - } - - let k; - - if (animationParams[0] <= tick.angle && tick.angle <= animationParams[1]) { - k = radius / (radius - this._getSize(tick.angle, animationParams[0], animationParams[1]) - delta); - } else { - k = radius / (radius - (size + delta)); - } - - const x1 = tick.x * (radius - size); - const y1 = tick.y * (radius - size); - const x2 = x1 * k; - const y2 = y1 * k; - - m.push({ x1, y1, x2, y2 }); - - if (i < 20) { - let scale = delta / (200 * scaleCoefficient); - scale = scale < 1 ? 1 : scale; - allScales.push(scale); - } - }); - - const scale = allScales.reduce((pv, cv) => pv + cv, 0) / allScales.length; - - return m.map(({ x1, y1, x2, y2 }) => ({ - x1: x1, - y1: y1, - x2: x2 * scale, - y2: y2 * scale, - })); - } - - _getSize (angle, l, r) { - const scaleCoefficient = this._getScaleCoefficient(); - const maxTickSize = TICK_SIZE * 9 * scaleCoefficient; - const m = (r - l) / 2; - const x = (angle - l); - - let h; - - if (x === m) { - return maxTickSize; - } - - const d = Math.abs(m - x); - const v = 40 * Math.sqrt(1 / d); - - if (v > maxTickSize) { - h = maxTickSize; - } else { - h = Math.max(TICK_SIZE, v); - } - - return h; - } - - _getTickPoints (count) { - const PI = 360; - const coords = []; - const step = PI / count; - - let rad; - - for(let deg = 0; deg < PI; deg += step) { - rad = deg * Math.PI / (PI / 2); - coords.push({ x: Math.cos(rad), y: -Math.sin(rad), angle: deg }); - } - - return coords; - } - - _drawTick (x1, y1, x2, y2) { - const cx = this._getCX(); - const cy = this._getCY(); - - const dx1 = Math.ceil(cx + x1); - const dy1 = Math.ceil(cy + y1); - const dx2 = Math.ceil(cx + x2); - const dy2 = Math.ceil(cy + y2); - - const gradient = this.canvasContext.createLinearGradient(dx1, dy1, dx2, dy2); - - const mainColor = this._getAccentColor(); - const lastColor = hex2rgba(mainColor, 0); - - gradient.addColorStop(0, mainColor); - gradient.addColorStop(0.6, mainColor); - gradient.addColorStop(1, lastColor); - - this.canvasContext.beginPath(); - this.canvasContext.strokeStyle = gradient; - this.canvasContext.lineWidth = 2; - this.canvasContext.moveTo(dx1, dy1); - this.canvasContext.lineTo(dx2, dy2); - this.canvasContext.stroke(); - } - _getCX() { return Math.floor(this.state.width / 2); } diff --git a/app/javascript/flavours/glitch/features/audio/visualizer.js b/app/javascript/flavours/glitch/features/audio/visualizer.js new file mode 100644 index 000000000..77d5b5a65 --- /dev/null +++ b/app/javascript/flavours/glitch/features/audio/visualizer.js @@ -0,0 +1,136 @@ +/* +Copyright (c) 2020 by Alex Permyakov (https://codepen.io/alexdevp/pen/RNELPV) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +const hex2rgba = (hex, alpha = 1) => { + const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; + +export default class Visualizer { + + constructor (tickSize) { + this.tickSize = tickSize; + } + + setCanvas(canvas) { + this.canvas = canvas; + if (canvas) { + this.context = canvas.getContext('2d'); + } + } + + setAudioContext(context, source) { + const analyser = context.createAnalyser(); + + analyser.smoothingTimeConstant = 0.6; + analyser.fftSize = 2048; + + source.connect(analyser); + + this.analyser = analyser; + } + + getTickPoints (count) { + const coords = []; + + for(let i = 0; i < count; i++) { + const rad = Math.PI * 2 * i / count; + coords.push({ x: Math.cos(rad), y: -Math.sin(rad) }); + } + + return coords; + } + + drawTick (cx, cy, mainColor, x1, y1, x2, y2) { + const dx1 = Math.ceil(cx + x1); + const dy1 = Math.ceil(cy + y1); + const dx2 = Math.ceil(cx + x2); + const dy2 = Math.ceil(cy + y2); + + const gradient = this.context.createLinearGradient(dx1, dy1, dx2, dy2); + + const lastColor = hex2rgba(mainColor, 0); + + gradient.addColorStop(0, mainColor); + gradient.addColorStop(0.6, mainColor); + gradient.addColorStop(1, lastColor); + + this.context.beginPath(); + this.context.strokeStyle = gradient; + this.context.lineWidth = 2; + this.context.moveTo(dx1, dy1); + this.context.lineTo(dx2, dy2); + this.context.stroke(); + } + + getTicks (count, size, radius, scaleCoefficient) { + const ticks = this.getTickPoints(count); + const lesser = 200; + const m = []; + const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0; + const frequencyData = new Uint8Array(bufferLength); + const allScales = []; + + if (this.analyser) { + this.analyser.getByteFrequencyData(frequencyData); + } + + ticks.forEach((tick, i) => { + const coef = 1 - i / (ticks.length * 2.5); + + let delta = ((frequencyData[i] || 0) - lesser * coef) * scaleCoefficient; + + if (delta < 0) { + delta = 0; + } + + const k = radius / (radius - (size + delta)); + + const x1 = tick.x * (radius - size); + const y1 = tick.y * (radius - size); + const x2 = x1 * k; + const y2 = y1 * k; + + m.push({ x1, y1, x2, y2 }); + + if (i < 20) { + let scale = delta / (200 * scaleCoefficient); + scale = scale < 1 ? 1 : scale; + allScales.push(scale); + } + }); + + const scale = allScales.reduce((pv, cv) => pv + cv, 0) / allScales.length; + + return m.map(({ x1, y1, x2, y2 }) => ({ + x1: x1, + y1: y1, + x2: x2 * scale, + y2: y2 * scale, + })); + } + + clear (width, height) { + this.context.clearRect(0, 0, width, height); + } + + draw (cx, cy, color, radius, coefficient) { + this.context.save(); + + const ticks = this.getTicks(parseInt(360 * coefficient), this.tickSize, radius, coefficient); + + ticks.forEach(tick => { + this.drawTick(cx, cy, color, tick.x1, tick.y1, tick.x2, tick.y2); + }); + + this.context.restore(); + } + +} -- cgit From 66c0953c33d6d872e720eb8dedc5ab94bb3cd69a Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Fri, 10 Jul 2020 03:32:36 +0700 Subject: [Glitch] Improve safety of Blurhash component Port 3ef94c00444f2b72a6f68e0fd9cff1b3f783c555 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/components/blurhash.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/components/blurhash.js b/app/javascript/flavours/glitch/components/blurhash.js index 172f8c2f5..2af5cfc56 100644 --- a/app/javascript/flavours/glitch/components/blurhash.js +++ b/app/javascript/flavours/glitch/components/blurhash.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; /** * @typedef BlurhashPropsBase - * @property {string} hash Hash to render + * @property {string?} hash Hash to render * @property {number} width * Width of the blurred region in pixels. Defaults to 32 * @property {number} [height] @@ -37,13 +37,17 @@ function Blurhash({ const { current: canvas } = canvasRef; canvas.width = canvas.width; // resets canvas - if (dummy) return; + if (dummy || !hash) return; - const pixels = decode(hash, width, height); - const ctx = canvas.getContext('2d'); - const imageData = new ImageData(pixels, width, height); + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); - ctx.putImageData(imageData, 0, 0); + ctx.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } }, [dummy, hash, width, height]); return ( -- cgit From 3fbcc4871f822712885f3f16b0c8b9b7e1e7780f Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 10 Jul 2020 18:04:18 +0200 Subject: [Glitch] Fix block/mute pagination in WebUI Port 38579b9f74cf75fa62345fc203bee8257d8a2119 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/reducers/user_lists.js | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'app/javascript/flavours') diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js index 3c56031dd..202f9198f 100644 --- a/app/javascript/flavours/glitch/reducers/user_lists.js +++ b/app/javascript/flavours/glitch/reducers/user_lists.js @@ -63,16 +63,16 @@ const initialState = ImmutableMap({ mutes: ImmutableMap(), }); -const normalizeList = (state, type, id, accounts, next) => { - return state.setIn([type, id], ImmutableMap({ +const normalizeList = (state, path, accounts, next) => { + return state.setIn(path, ImmutableMap({ next, items: ImmutableList(accounts.map(item => item.id)), isLoading: false, })); }; -const appendToList = (state, type, id, accounts, next) => { - return state.updateIn([type, id], map => { +const appendToList = (state, path, accounts, next) => { + return state.updateIn(path, map => { return map.set('next', next).set('isLoading', false).update('items', list => list.concat(accounts.map(item => item.id))); }); }; @@ -86,9 +86,9 @@ const normalizeFollowRequest = (state, notification) => { export default function userLists(state = initialState, action) { switch(action.type) { case FOLLOWERS_FETCH_SUCCESS: - return normalizeList(state, 'followers', action.id, action.accounts, action.next); + return normalizeList(state, ['followers', action.id], action.accounts, action.next); case FOLLOWERS_EXPAND_SUCCESS: - return appendToList(state, 'followers', action.id, action.accounts, action.next); + return appendToList(state, ['followers', action.id], action.accounts, action.next); case FOLLOWERS_FETCH_REQUEST: case FOLLOWERS_EXPAND_REQUEST: return state.setIn(['followers', action.id, 'isLoading'], true); @@ -96,9 +96,9 @@ export default function userLists(state = initialState, action) { case FOLLOWERS_EXPAND_FAIL: return state.setIn(['followers', action.id, 'isLoading'], false); case FOLLOWING_FETCH_SUCCESS: - return normalizeList(state, 'following', action.id, action.accounts, action.next); + return normalizeList(state, ['following', action.id], action.accounts, action.next); case FOLLOWING_EXPAND_SUCCESS: - return appendToList(state, 'following', action.id, action.accounts, action.next); + return appendToList(state, ['following', action.id], action.accounts, action.next); case FOLLOWING_FETCH_REQUEST: case FOLLOWING_EXPAND_REQUEST: return state.setIn(['following', action.id, 'isLoading'], true); @@ -112,9 +112,9 @@ export default function userLists(state = initialState, action) { case NOTIFICATIONS_UPDATE: return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; case FOLLOW_REQUESTS_FETCH_SUCCESS: - return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next).setIn(['follow_requests', 'isLoading'], false); + return normalizeList(state, ['follow_requests'], action.accounts, action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: - return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next).setIn(['follow_requests', 'isLoading'], false); + return appendToList(state, ['follow_requests'], action.accounts, action.next); case FOLLOW_REQUESTS_FETCH_REQUEST: case FOLLOW_REQUESTS_EXPAND_REQUEST: return state.setIn(['follow_requests', 'isLoading'], true); @@ -125,9 +125,9 @@ export default function userLists(state = initialState, action) { case FOLLOW_REQUEST_REJECT_SUCCESS: return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); case BLOCKS_FETCH_SUCCESS: - return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + return normalizeList(state, ['blocks'], action.accounts, action.next); case BLOCKS_EXPAND_SUCCESS: - return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + return appendToList(state, ['blocks'], action.accounts, action.next); case BLOCKS_FETCH_REQUEST: case BLOCKS_EXPAND_REQUEST: return state.setIn(['blocks', 'isLoading'], true); @@ -135,9 +135,9 @@ export default function userLists(state = initialState, action) { case BLOCKS_EXPAND_FAIL: return state.setIn(['blocks', 'isLoading'], false); case MUTES_FETCH_SUCCESS: - return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + return normalizeList(state, ['mutes'], action.accounts, action.next); case MUTES_EXPAND_SUCCESS: - return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + return appendToList(state, ['mutes'], action.accounts, action.next); case MUTES_FETCH_REQUEST: case MUTES_EXPAND_REQUEST: return state.setIn(['mutes', 'isLoading'], true); @@ -145,9 +145,9 @@ export default function userLists(state = initialState, action) { case MUTES_EXPAND_FAIL: return state.setIn(['mutes', 'isLoading'], false); case DIRECTORY_FETCH_SUCCESS: - return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false); + return normalizeList(state, ['directory'], action.accounts, action.next); case DIRECTORY_EXPAND_SUCCESS: - return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false); + return appendToList(state, ['directory'], action.accounts, action.next); case DIRECTORY_FETCH_REQUEST: case DIRECTORY_EXPAND_REQUEST: return state.setIn(['directory', 'isLoading'], true); -- cgit