diff options
author | Starfall <us@starfall.systems> | 2020-07-11 12:31:22 -0500 |
---|---|---|
committer | Starfall <us@starfall.systems> | 2020-07-11 12:31:22 -0500 |
commit | 2f20bc2a8275875033c97249825a2a3305980c3b (patch) | |
tree | 079a021ab1ce792a40555e0718f9861ee8b53f7a /app/javascript/flavours/glitch/components | |
parent | 816d10c7eecd83cb0f115c10328cbb504dabc7e9 (diff) | |
parent | 7a23347db5be3f262dbcafbecf768588dc648bda (diff) |
Merge branch 'glitch' into main
Diffstat (limited to 'app/javascript/flavours/glitch/components')
11 files changed, 348 insertions, 49 deletions
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 && ( + <ShortNumber + value={tag.history.reduce((total, day) => total + day.uses * 1, 0)} + /> + ); return ( <div className='autosuggest-hashtag'> - <div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div> - {tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>} + <div className='autosuggest-hashtag__name'> + #<strong>{tag.name}</strong> + </div> + {tag.history !== undefined && ( + <div className='autosuggest-hashtag__uses'> + <FormattedMessage + id='autosuggest_hashtag.per_week' + defaultMessage='{count} per week' + values={{ count: weeklyUses }} + /> + </div> + )} </div> ); } diff --git a/app/javascript/flavours/glitch/components/blurhash.js b/app/javascript/flavours/glitch/components/blurhash.js new file mode 100644 index 000000000..2af5cfc56 --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.js @@ -0,0 +1,65 @@ +// @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<HTMLCanvasElement>} */ (useRef()); + + useEffect(() => { + const { current: canvas } = canvasRef; + canvas.width = canvas.width; // resets canvas + + if (dummy || !hash) return; + + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + ctx.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } + }, [dummy, hash, width, height]); + + return ( + <canvas {...canvasProps} ref={canvasRef} width={width} height={height} /> + ); +} + +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/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) => <strong>{displayNumber}</strong> + : (displayNumber) => displayNumber; + + switch (counterType) { + case 'statuses': { + return (displayNumber, pluralReady) => ( + <FormattedMessage + id='account.statuses_counter' + defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}' + values={{ + count: pluralReady, + counter: renderCounter(displayNumber), + }} + /> + ); + } + case 'following': { + return (displayNumber, pluralReady) => ( + <FormattedMessage + id='account.following_counter' + defaultMessage='{count, plural, other {{counter} Following}}' + values={{ + count: pluralReady, + counter: renderCounter(displayNumber), + }} + /> + ); + } + case 'followers': { + return (displayNumber, pluralReady) => ( + <FormattedMessage + id='account.followers_counter' + defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}' + values={{ + count: pluralReady, + counter: renderCounter(displayNumber), + }} + /> + ); + } + 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) => ( + <FormattedMessage + id='trends.counter_by_accounts' + defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking' + values={{ + count: pluralReady, + counter: <strong>{displayNumber}</strong>, + }} + /> +); const Hashtag = ({ hashtag }) => ( <div className='trends__item'> <div className='trends__item__name'> - <Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}> + <Permalink + href={hashtag.get('url')} + to={`/timelines/tag/${hashtag.get('name')}`} + > #<span>{hashtag.get('name')}</span> </Permalink> - <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} /> + <ShortNumber + value={ + hashtag.getIn(['history', 0, 'accounts']) * 1 + + hashtag.getIn(['history', 1, 'accounts']) * 1 + } + renderer={accountsCountRenderer} + /> </div> <div className='trends__item__current'> - {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)} + <ShortNumber + value={ + hashtag.getIn(['history', 0, 'uses']) * 1 + + hashtag.getIn(['history', 1, 'uses']) * 1 + } + /> </div> <div className='trends__item__sparkline'> - <Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}> + <Sparklines + width={50} + height={28} + data={hashtag + .get('history') + .reverse() + .map((day) => day.get('uses')) + .toArray()} + > <SparklinesCurve style={{ fill: 'none' }} /> </Sparklines> </div> 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 ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> - <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> + <Blurhash + hash={attachment.get('blurhash')} + className='media-gallery__preview' + dummy={!useBlurhash} + /> </a> </div> ); @@ -253,7 +227,13 @@ class Item extends React.PureComponent { return ( <div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} /> + <Blurhash + hash={attachment.get('blurhash')} + dummy={!useBlurhash} + className={classNames('media-gallery__preview', { + 'media-gallery__preview--hidden': visible && this.state.loaded, + })} + /> {visible && thumbnail} </div> ); diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index fae0a7393..5d10ed650 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -10,10 +10,18 @@ import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; import LoadingIndicator from './loading_indicator'; +import { connect } from 'react-redux'; const MOUSE_IDLE_DELAY = 300; -export default class ScrollableList extends PureComponent { +const mapStateToProps = (state, { scrollKey }) => { + return { + preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']), + }; +}; + +export default @connect(mapStateToProps) +class ScrollableList extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -37,6 +45,7 @@ export default class ScrollableList extends PureComponent { emptyMessage: PropTypes.node, children: PropTypes.node, bindToDocument: PropTypes.bool, + preventScroll: PropTypes.bool, }; static defaultProps = { @@ -124,7 +133,7 @@ export default class ScrollableList extends PureComponent { }); handleMouseIdle = () => { - if (this.scrollToTopOnMouseIdle) { + if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) { this.setScrollTop(0); } this.mouseMovedRecently = false; @@ -176,7 +185,7 @@ export default class ScrollableList extends PureComponent { this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); - if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) { + if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) { return this.getScrollHeight() - this.getScrollTop(); } else { return null; 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 = <ShortNumberCounter value={shortNumber} />; + + // 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 = ( + <FormattedNumber + value={rawNumber} + maximumFractionDigits={maxFractionDigits} + /> + ); + + let values = { count, rawNumber }; + + switch (unit) { + case DECIMAL_UNITS.THOUSAND: { + return ( + <FormattedMessage + id='units.short.thousand' + defaultMessage='{count}K' + values={values} + /> + ); + } + case DECIMAL_UNITS.MILLION: { + return ( + <FormattedMessage + id='units.short.million' + defaultMessage='{count}M' + values={values} + /> + ); + } + case DECIMAL_UNITS.BILLION: { + return ( + <FormattedMessage + id='units.short.billion' + defaultMessage='{count}B' + values={values} + /> + ); + } + // 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/components/status.js b/app/javascript/flavours/glitch/components/status.js index ba0823a60..4e628a420 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -96,6 +96,7 @@ class Status extends ImmutablePureComponent { cacheMediaWidth: PropTypes.func, cachedMediaWidth: PropTypes.number, onClick: PropTypes.func, + scrollKey: PropTypes.string, }; state = { diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 0a481c816..c314c5fd5 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -74,6 +74,7 @@ class StatusActionBar extends ImmutablePureComponent { withDismiss: PropTypes.bool, showReplyCount: PropTypes.bool, directMessage: PropTypes.bool, + scrollKey: PropTypes.string, intl: PropTypes.object.isRequired, }; @@ -198,7 +199,7 @@ class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss, showReplyCount, directMessage } = this.props; + const { status, intl, withDismiss, showReplyCount, directMessage, scrollKey } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; @@ -300,7 +301,16 @@ class StatusActionBar extends ImmutablePureComponent { <IconButton key='bookmark-button' className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />, filterButton, <div key='dropdown-button' className='status__action-bar-dropdown'> - <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> + <DropdownMenuContainer + scrollKey={scrollKey} + disabled={anonymousAccess} + status={status} + items={menu} + icon='ellipsis-h' + size={18} + direction='right' + ariaLabel={intl.formatMessage(messages.more)} + /> </div>, ]} 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; diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index a399ff567..60cc23f4b 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -99,6 +99,7 @@ export default class StatusList extends ImmutablePureComponent { onMoveUp={this.handleMoveUp} onMoveDown={this.handleMoveDown} contextType={timelineId} + scrollKey={this.props.scrollKey} /> )) ) : null; @@ -112,6 +113,7 @@ export default class StatusList extends ImmutablePureComponent { onMoveUp={this.handleMoveUp} onMoveDown={this.handleMoveDown} contextType={timelineId} + scrollKey={this.props.scrollKey} /> )).concat(scrollableContent); } |