diff options
author | Starfall <us@starfall.systems> | 2022-11-10 08:50:11 -0600 |
---|---|---|
committer | Starfall <us@starfall.systems> | 2022-11-10 08:50:11 -0600 |
commit | 67d1a0476d77e2ed0ca15dd2981c54c2b90b0742 (patch) | |
tree | 152f8c13a341d76738e8e2c09b24711936e6af68 /app/javascript/flavours/glitch/components | |
parent | b581e6b6d4a5ba9ed4ae17427b7f2d5d158be4e5 (diff) | |
parent | ee7e49d1b1323618e16026bc8db8ab7f9459cc2d (diff) |
Merge remote-tracking branch 'glitch/main'
- Remove Helm charts - Lots of conflicts with our removal of recommended settings and custom icons
Diffstat (limited to 'app/javascript/flavours/glitch/components')
39 files changed, 435 insertions, 168 deletions
diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 489f60736..8e810ce5f 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -7,8 +7,9 @@ import Permalink from './permalink'; import IconButton from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me } from 'flavours/glitch/initial_state'; import RelativeTimestamp from './relative_timestamp'; +import Skeleton from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -26,7 +27,8 @@ export default @injectIntl class Account extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + size: PropTypes.number, + account: ImmutablePropTypes.map, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, @@ -40,6 +42,10 @@ class Account extends ImmutablePureComponent { onActionClick: PropTypes.func, }; + static defaultProps = { + size: 36, + }; + handleFollow = () => { this.props.onFollow(this.props.account); } @@ -74,10 +80,20 @@ class Account extends ImmutablePureComponent { actionIcon, actionTitle, defaultAction, + size, } = this.props; if (!account) { - return <div />; + return ( + <div className='account'> + <div className='account__wrapper'> + <div className='account__display-name'> + <div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div> + <DisplayName /> + </div> + </div> + </div> + ); } if (hidden) { @@ -153,7 +169,7 @@ class Account extends ImmutablePureComponent { <div className='account'> <div className='account__wrapper'> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}> - <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> + <div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div> {mute_expires_at} <DisplayName account={account} /> </Permalink> diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js index a4d6cef41..5b6a19f8d 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.js +++ b/app/javascript/flavours/glitch/components/admin/Counter.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/util/api'; +import api from 'flavours/glitch/api'; import { FormattedNumber } from 'react-intl'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.js index a924d093c..3dac8c6c2 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.js +++ b/app/javascript/flavours/glitch/components/admin/Dimension.js @@ -1,8 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/util/api'; +import api from 'flavours/glitch/api'; import { FormattedNumber } from 'react-intl'; -import { roundTo10 } from 'flavours/glitch/util/numbers'; +import { roundTo10 } from 'flavours/glitch/utils/numbers'; import Skeleton from 'flavours/glitch/components/skeleton'; export default class Dimension extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js index 0f2a4fe36..771dbb452 100644 --- a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/util/api'; +import api from 'flavours/glitch/api'; import { injectIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/components/admin/Retention.js b/app/javascript/flavours/glitch/components/admin/Retention.js index 6d7e4b279..9cc39040b 100644 --- a/app/javascript/flavours/glitch/components/admin/Retention.js +++ b/app/javascript/flavours/glitch/components/admin/Retention.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/util/api'; +import api from 'flavours/glitch/api'; import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; import classNames from 'classnames'; -import { roundTo10 } from 'flavours/glitch/util/numbers'; +import { roundTo10 } from 'flavours/glitch/utils/numbers'; const dateForCohort = cohort => { switch(cohort.frequency) { diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js index 60e367f00..4c17b69a0 100644 --- a/app/javascript/flavours/glitch/components/admin/Trends.js +++ b/app/javascript/flavours/glitch/components/admin/Trends.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/util/api'; +import api from 'flavours/glitch/api'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import Hashtag from 'flavours/glitch/components/hashtag'; diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js index 3cc5173dd..9431c96f7 100644 --- a/app/javascript/flavours/glitch/components/animated_number.js +++ b/app/javascript/flavours/glitch/components/animated_number.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FormattedNumber } from 'react-intl'; +import ShortNumber from 'mastodon/components/short_number'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; -import { reduceMotion } from 'flavours/glitch/util/initial_state'; +import { reduceMotion } from 'flavours/glitch/initial_state'; const obfuscatedCount = count => { if (count < 0) { @@ -51,7 +51,7 @@ export default class AnimatedNumber extends React.PureComponent { const { direction } = this.state; if (reduceMotion) { - return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />; + return obfuscate ? obfuscatedCount(value) : <ShortNumber value={value} />; } const styles = [{ @@ -65,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( <span className='animated-number'> {items.map(({ key, data, style }) => ( - <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span> + <span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}</span> ))} </span> )} diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js index d04c1eb68..83fafbd10 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.js +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.js @@ -1,8 +1,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light'; +import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; -import { assetHost } from 'flavours/glitch/util/config'; +import { assetHost } from 'flavours/glitch/utils/config'; export default class AutosuggestEmoji extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js index 6d53a5298..38fd99af5 100644 --- a/app/javascript/flavours/glitch/components/avatar.js +++ b/app/javascript/flavours/glitch/components/avatar.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; import classNames from 'classnames'; export default class Avatar extends React.PureComponent { @@ -70,6 +70,8 @@ export default class Avatar extends React.PureComponent { onMouseLeave={this.handleMouseLeave} style={style} data-avatar-of={account && `@${account.get('acct')}`} + role='img' + aria-label={account?.get('acct')} /> ); } diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.js index e30dfe68a..c0ce7761d 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.js +++ b/app/javascript/flavours/glitch/components/avatar_composite.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; export default class AvatarComposite extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/avatar_overlay.js b/app/javascript/flavours/glitch/components/avatar_overlay.js index 23db5182b..01dec587a 100644 --- a/app/javascript/flavours/glitch/components/avatar_overlay.js +++ b/app/javascript/flavours/glitch/components/avatar_overlay.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; export default class AvatarOverlay extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.js index c9da7d329..cf0e6d5e4 100644 --- a/app/javascript/flavours/glitch/components/column.js +++ b/app/javascript/flavours/glitch/components/column.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { supportsPassiveEvents } from 'detect-passive-events'; -import { scrollTop } from 'flavours/glitch/util/scroll'; +import { scrollTop } from '../scroll'; export default class Column extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index 500612093..0f89b3a97 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -17,6 +17,7 @@ class ColumnHeader extends React.PureComponent { static contextTypes = { router: PropTypes.object, + identity: PropTypes.object, }; static propTypes = { @@ -62,7 +63,7 @@ class ColumnHeader extends React.PureComponent { } handleTitleClick = () => { - this.props.onClick(); + this.props.onClick?.(); } handleMoveLeft = () => { @@ -150,13 +151,12 @@ class ColumnHeader extends React.PureComponent { collapsedContent.push(moveButtons); } - if (children || (multiColumn && this.props.onPin)) { + if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) { collapseButton = ( <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} - aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick} > <i className='icon-with-badge'> diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.js b/app/javascript/flavours/glitch/components/dismissable_banner.js new file mode 100644 index 000000000..ff52a619d --- /dev/null +++ b/app/javascript/flavours/glitch/components/dismissable_banner.js @@ -0,0 +1,51 @@ +import React from 'react'; +import IconButton from './icon_button'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import { bannerSettings } from 'flavours/glitch/settings'; + +const messages = defineMessages({ + dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, +}); + +export default @injectIntl +class DismissableBanner extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.node, + intl: PropTypes.object.isRequired, + }; + + state = { + visible: !bannerSettings.get(this.props.id), + }; + + handleDismiss = () => { + const { id } = this.props; + this.setState({ visible: false }, () => bannerSettings.set(id, true)); + } + + render () { + const { visible } = this.state; + + if (!visible) { + return null; + } + + const { children, intl } = this.props; + + return ( + <div className='dismissable-banner'> + <div className='dismissable-banner__message'> + {children} + </div> + + <div className='dismissable-banner__action'> + <IconButton icon='times' title={intl.formatMessage(messages.dismiss)} onClick={this.handleDismiss} /> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js index 9c7da744e..1c2297578 100644 --- a/app/javascript/flavours/glitch/components/display_name.js +++ b/app/javascript/flavours/glitch/components/display_name.js @@ -2,7 +2,8 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; +import Skeleton from 'flavours/glitch/components/skeleton'; export default class DisplayName extends React.PureComponent { @@ -46,14 +47,15 @@ export default class DisplayName extends React.PureComponent { const computedClass = classNames('display-name', { inline }, className); - if (!account) return null; - let displayName, suffix; + let acct; - let acct = account.get('acct'); + if (account) { + acct = account.get('acct'); - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; + if (acct.indexOf('@') === -1 && localDomain) { + acct = `${acct}@${localDomain}`; + } } if (others && others.size > 0) { @@ -80,9 +82,12 @@ export default class DisplayName extends React.PureComponent { <span className='display-name__account'>@{acct}</span> </a> ); - } else { + } else if (account) { displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>; suffix = <span className='display-name__account'>@{acct}</span>; + } else { + displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>; + suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>; } return ( diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index e04af8074..036e0b909 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import IconButton from './icon_button'; import Overlay from 'react-overlays/lib/Overlay'; -import Motion from 'flavours/glitch/util/optional_motion'; +import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { supportsPassiveEvents } from 'detect-passive-events'; import classNames from 'classnames'; diff --git a/app/javascript/flavours/glitch/components/error_boundary.js b/app/javascript/flavours/glitch/components/error_boundary.js index 4537bde1d..e0ca3e2b0 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.js +++ b/app/javascript/flavours/glitch/components/error_boundary.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; -import { source_url } from 'flavours/glitch/util/initial_state'; -import { preferencesLink } from 'flavours/glitch/util/backend_links'; +import { source_url } from 'flavours/glitch/initial_state'; +import { preferencesLink } from 'flavours/glitch/utils/backend_links'; import StackTrace from 'stacktrace-js'; +import { Helmet } from 'react-helmet'; export default class ErrorBoundary extends React.PureComponent { @@ -122,6 +123,10 @@ export default class ErrorBoundary extends React.PureComponent { )} </ul> </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </div> ); } diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 5bbf32c87..422b9a8fa 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -1,7 +1,7 @@ // @ts-check import React from 'react'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; @@ -9,10 +9,6 @@ import ShortNumber from 'flavours/glitch/components/short_number'; import Skeleton from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; -const messages = defineMessages({ - totalVolume: { id: 'hashtag.total_volume', defaultMessage: 'Total volume in the last {days, plural, one {day} other {{days} days}}' }, -}); - class SilentErrorBoundary extends React.Component { static propTypes = { @@ -42,7 +38,7 @@ class SilentErrorBoundary extends React.Component { * * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} */ -const accountsCountRenderer = (displayNumber, pluralReady) => ( +export const accountsCountRenderer = (displayNumber, pluralReady) => ( <FormattedMessage id='trends.counter_by_accounts' defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}' @@ -60,7 +56,6 @@ export const ImmutableHashtag = ({ hashtag }) => ( href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`} people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1} - uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1} history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()} /> ); @@ -69,39 +64,52 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; -const Hashtag = injectIntl(({ name, href, to, people, uses, history, className, intl }) => ( +const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => ( <div className={classNames('trends__item', className)}> <div className='trends__item__name'> <Permalink href={href} to={to}> {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />} </Permalink> - {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />} + {description ? ( + <span>{description}</span> + ) : ( + typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} /> + )} </div> - <abbr className='trends__item__current' title={intl.formatMessage(messages.totalVolume, { days: 2 })}> - {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />} - <span className='trends__item__current__asterisk'>*</span> - </abbr> - - <div className='trends__item__sparkline'> - <SilentErrorBoundary> - <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> - <SparklinesCurve style={{ fill: 'none' }} /> - </Sparklines> - </SilentErrorBoundary> - </div> + {typeof uses !== 'undefined' && ( + <div className='trends__item__current'> + <ShortNumber value={uses} /> + </div> + )} + + {withGraph && ( + <div className='trends__item__sparkline'> + <SilentErrorBoundary> + <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> + <SparklinesCurve style={{ fill: 'none' }} /> + </Sparklines> + </SilentErrorBoundary> + </div> + )} </div> -)); +); Hashtag.propTypes = { name: PropTypes.string, href: PropTypes.string, to: PropTypes.string, people: PropTypes.number, + description: PropTypes.node, uses: PropTypes.number, history: PropTypes.arrayOf(PropTypes.number), className: PropTypes.string, + withGraph: PropTypes.bool, +}; + +Hashtag.defaultProps = { + withGraph: true, }; export default Hashtag; diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 9ff745355..41a95e92f 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -1,5 +1,5 @@ import React from 'react'; -import Motion from 'flavours/glitch/util/optional_motion'; +import Motion from '../features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import PropTypes from 'prop-types'; import classNames from 'classnames'; @@ -18,7 +18,6 @@ export default class IconButton extends React.PureComponent { onKeyPress: PropTypes.func, size: PropTypes.number, active: PropTypes.bool, - pressed: PropTypes.bool, expanded: PropTypes.bool, style: PropTypes.object, activeStyle: PropTypes.object, @@ -84,15 +83,21 @@ export default class IconButton extends React.PureComponent { } render () { + // Hack required for some icons which have an overriden size + let containerSize = '1.28571429em'; + if (this.props.style?.fontSize) { + containerSize = `${this.props.size * 1.28571429}px`; + } + let style = { fontSize: `${this.props.size}px`, - height: '1.28571429em', + height: containerSize, lineHeight: `${this.props.size}px`, ...this.props.style, ...(this.props.active ? this.props.activeStyle : {}), }; if (!this.props.label) { - style.width = '1.28571429em'; + style.width = containerSize; } else { style.textAlign = 'left'; } @@ -105,7 +110,6 @@ export default class IconButton extends React.PureComponent { icon, inverted, overlay, - pressed, tabIndex, title, counter, @@ -150,7 +154,6 @@ export default class IconButton extends React.PureComponent { return ( <button aria-label={title} - aria-pressed={pressed} aria-expanded={expanded} title={title} className={classes} diff --git a/app/javascript/flavours/glitch/components/image.js b/app/javascript/flavours/glitch/components/image.js new file mode 100644 index 000000000..6e81ddf08 --- /dev/null +++ b/app/javascript/flavours/glitch/components/image.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Blurhash from './blurhash'; +import classNames from 'classnames'; + +export default class Image extends React.PureComponent { + + static propTypes = { + src: PropTypes.string, + srcSet: PropTypes.string, + blurhash: PropTypes.string, + className: PropTypes.string, + }; + + state = { + loaded: false, + }; + + handleLoad = () => this.setState({ loaded: true }); + + render () { + const { src, srcSet, blurhash, className } = this.props; + const { loaded } = this.state; + + return ( + <div className={classNames('image', { loaded }, className)} role='presentation'> + {blurhash && <Blurhash hash={blurhash} className='image__preview' />} + <img src={src} srcSet={srcSet} alt='' onLoad={this.handleLoad} /> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.js b/app/javascript/flavours/glitch/components/intersection_observer_article.js index 88f29892e..b28e44e4c 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.js +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; -import getRectFromEntry from 'flavours/glitch/util/get_rect_from_entry'; +import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; // Diff these props in the "unrendered" state const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; @@ -94,7 +94,7 @@ export default class IntersectionObserverArticle extends React.Component { // When the browser gets a chance, test if we're still not intersecting, // and if so, set our isHidden to true to trigger an unrender. The point of // this is to save DOM nodes and avoid using up too much memory. - // See: https://github.com/tootsuite/mastodon/issues/2900 + // See: https://github.com/mastodon/mastodon/issues/2900 this.setState((prevState) => ({ isHidden: !prevState.isIntersecting })); } diff --git a/app/javascript/flavours/glitch/components/link.js b/app/javascript/flavours/glitch/components/link.js index de99f7d42..bbec121a8 100644 --- a/app/javascript/flavours/glitch/components/link.js +++ b/app/javascript/flavours/glitch/components/link.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; import React from 'react'; // Utils. -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; +import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; // Handlers. const handlers = { diff --git a/app/javascript/flavours/glitch/components/logo.js b/app/javascript/flavours/glitch/components/logo.js index d1c7f08a9..ee5c22496 100644 --- a/app/javascript/flavours/glitch/components/logo.js +++ b/app/javascript/flavours/glitch/components/logo.js @@ -1,8 +1,9 @@ import React from 'react'; const Logo = () => ( - <svg viewBox='0 0 216.4144 232.00976' className='logo'> - <use xlinkHref='#mastodon-svg-logo' /> + <svg viewBox='0 0 261 66' className='logo' role='img'> + <title>Mastodon</title> + <use xlinkHref='#logo-symbol-wordmark' /> </svg> ); diff --git a/app/javascript/flavours/glitch/components/media_attachments.js b/app/javascript/flavours/glitch/components/media_attachments.js index c8d133f09..a517fcf30 100644 --- a/app/javascript/flavours/glitch/components/media_attachments.js +++ b/app/javascript/flavours/glitch/components/media_attachments.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components'; +import { MediaGallery, Video, Audio } from 'flavours/glitch/features/ui/util/async-components'; import Bundle from 'flavours/glitch/features/ui/components/bundle'; import noop from 'lodash/noop'; diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 68195ea80..5414b4858 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -4,9 +4,9 @@ import PropTypes from 'prop-types'; import { is } from 'immutable'; import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { isIOS } from 'flavours/glitch/util/is_mobile'; +import { isIOS } from '../is_mobile'; import classNames from 'classnames'; -import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import { debounce } from 'lodash'; import Blurhash from 'flavours/glitch/components/blurhash'; diff --git a/app/javascript/flavours/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js index ee5bf7c1e..08e39c236 100644 --- a/app/javascript/flavours/glitch/components/missing_indicator.js +++ b/app/javascript/flavours/glitch/components/missing_indicator.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg'; import classNames from 'classnames'; +import { Helmet } from 'react-helmet'; const MissingIndicator = ({ fullPage }) => ( <div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}> @@ -14,6 +15,10 @@ const MissingIndicator = ({ fullPage }) => ( <FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' /> <FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' /> </div> + + <Helmet> + <meta name='robots' content='noindex' /> + </Helmet> </div> ); diff --git a/app/javascript/flavours/glitch/components/navigation_portal.js b/app/javascript/flavours/glitch/components/navigation_portal.js new file mode 100644 index 000000000..90afa1da0 --- /dev/null +++ b/app/javascript/flavours/glitch/components/navigation_portal.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { Switch, Route, withRouter } from 'react-router-dom'; +import { showTrends } from 'flavours/glitch/initial_state'; +import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; +import AccountNavigation from 'flavours/glitch/features/account/navigation'; + +const DefaultNavigation = () => ( + <> + {showTrends && ( + <> + <div className='flex-spacer' /> + <Trends /> + </> + )} + </> +); + +export default @withRouter +class NavigationPortal extends React.PureComponent { + + render () { + return ( + <Switch> + <Route path='/@:acct' exact component={AccountNavigation} /> + <Route path='/@:acct/tagged/:tagged?' exact component={AccountNavigation} /> + <Route path='/@:acct/with_replies' exact component={AccountNavigation} /> + <Route path='/@:acct/followers' exact component={AccountNavigation} /> + <Route path='/@:acct/following' exact component={AccountNavigation} /> + <Route path='/@:acct/media' exact component={AccountNavigation} /> + <Route component={DefaultNavigation} /> + </Switch> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.js b/app/javascript/flavours/glitch/components/not_signed_in_indicator.js new file mode 100644 index 000000000..b440c6be2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +const NotSignedInIndicator = () => ( + <div className='scrollable scrollable--flex'> + <div className='empty-column-indicator'> + <FormattedMessage id='not_signed_in_indicator.not_signed_in' defaultMessage='You need to sign in to access this resource.' /> + </div> + </div> +); + +export default NotSignedInIndicator; diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index 970b00705..da65cd241 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -4,10 +4,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import Motion from 'flavours/glitch/util/optional_motion'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; -import emojify from 'flavours/glitch/util/emoji'; +import emojify from 'flavours/glitch/features/emoji/emoji'; import RelativeTimestamp from './relative_timestamp'; import Icon from 'flavours/glitch/components/icon'; @@ -34,6 +34,10 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { export default @injectIntl class Poll extends ImmutablePureComponent { + static contextTypes = { + identity: PropTypes.object, + }; + static propTypes = { poll: ImmutablePropTypes.map, intl: PropTypes.object.isRequired, @@ -217,7 +221,7 @@ class Poll extends ImmutablePureComponent { </ul> <div className='poll__footer'> - {!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>} + {!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>} {showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>} {votesCount} {poll.get('expires_at') && <span> · {timeRemaining}</span>} diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 50bfacc6a..8eb2b66d4 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -4,11 +4,11 @@ import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import LoadMore from './load_more'; import LoadPending from './load_pending'; -import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper'; +import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; import LoadingIndicator from './loading_indicator'; import { connect } from 'react-redux'; diff --git a/app/javascript/flavours/glitch/components/server_banner.js b/app/javascript/flavours/glitch/components/server_banner.js new file mode 100644 index 000000000..36e0ff238 --- /dev/null +++ b/app/javascript/flavours/glitch/components/server_banner.js @@ -0,0 +1,93 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { fetchServer } from 'flavours/glitch/actions/server'; +import ShortNumber from 'flavours/glitch/components/short_number'; +import Skeleton from 'flavours/glitch/components/skeleton'; +import Account from 'flavours/glitch/containers/account_container'; +import { domain } from 'flavours/glitch/initial_state'; +import Image from 'flavours/glitch/components/image'; +import { Link } from 'react-router-dom'; + +const messages = defineMessages({ + aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, +}); + +const mapStateToProps = state => ({ + server: state.getIn(['server', 'server']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class ServerBanner extends React.PureComponent { + + static propTypes = { + server: PropTypes.object, + dispatch: PropTypes.func, + intl: PropTypes.object, + }; + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchServer()); + } + + render () { + const { server, intl } = this.props; + const isLoading = server.get('isLoading'); + + return ( + <div className='server-banner'> + <div className='server-banner__introduction'> + <FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} /> + </div> + + <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' /> + + <div className='server-banner__description'> + {isLoading ? ( + <> + <Skeleton width='100%' /> + <br /> + <Skeleton width='100%' /> + <br /> + <Skeleton width='70%' /> + </> + ) : server.get('description')} + </div> + + <div className='server-banner__meta'> + <div className='server-banner__meta__column'> + <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4> + + <Account id={server.getIn(['contact', 'account', 'id'])} size={36} /> + </div> + + <div className='server-banner__meta__column'> + <h4><FormattedMessage id='server_banner.server_stats' defaultMessage='Server stats:' /></h4> + + {isLoading ? ( + <> + <strong className='server-banner__number'><Skeleton width='10ch' /></strong> + <br /> + <span className='server-banner__number-label'><Skeleton width='5ch' /></span> + </> + ) : ( + <> + <strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong> + <br /> + <span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span> + </> + )} + </div> + </div> + + <hr className='spacer' /> + + <Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/short_number.js b/app/javascript/flavours/glitch/components/short_number.js index e4ba09634..535c17727 100644 --- a/app/javascript/flavours/glitch/components/short_number.js +++ b/app/javascript/flavours/glitch/components/short_number.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../util/numbers'; +import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; import { FormattedMessage, FormattedNumber } from 'react-intl'; // @ts-check @@ -56,7 +56,7 @@ ShortNumber.propTypes = { /** * @typedef {object} ShortNumberCounterProps - * @property {import('../util/number').ShortNumber} value Short number + * @property {import('../utils/number').ShortNumber} value Short number */ /** diff --git a/app/javascript/flavours/glitch/components/skeleton.js b/app/javascript/flavours/glitch/components/skeleton.js index 09093e99c..6a17ffb26 100644 --- a/app/javascript/flavours/glitch/components/skeleton.js +++ b/app/javascript/flavours/glitch/components/skeleton.js @@ -4,8 +4,8 @@ import PropTypes from 'prop-types'; const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>; Skeleton.propTypes = { - width: PropTypes.number, - height: PropTypes.number, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; export default Skeleton; diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index e238456c5..366a98d82 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -10,13 +10,13 @@ import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components'; +import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import classNames from 'classnames'; -import { autoUnfoldCW } from 'flavours/glitch/util/content_warning'; +import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; import PollContainer from 'flavours/glitch/containers/poll_container'; -import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { displayMedia } from 'flavours/glitch/initial_state'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; // We use the component (and not the container) since we do not want @@ -83,6 +83,7 @@ class Status extends ImmutablePureComponent { onEmbed: PropTypes.func, onHeightChange: PropTypes.func, onToggleHidden: PropTypes.func, + onInteractionModal: PropTypes.func, muted: PropTypes.bool, hidden: PropTypes.bool, unread: PropTypes.bool, @@ -99,8 +100,11 @@ class Status extends ImmutablePureComponent { onClick: PropTypes.func, scrollKey: PropTypes.string, deployPictureInPicture: PropTypes.func, - usingPiP: PropTypes.bool, settings: ImmutablePropTypes.map.isRequired, + pictureInPicture: PropTypes.shape({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), }; state = { @@ -126,7 +130,7 @@ class Status extends ImmutablePureComponent { 'hidden', 'expanded', 'unread', - 'usingPiP', + 'pictureInPicture', ] updateOnStates = [ @@ -495,7 +499,6 @@ class Status extends ImmutablePureComponent { settings, collapsed, muted, - prepend, intersectionObserverWrapper, onOpenVideo, onOpenMedia, @@ -503,7 +506,7 @@ class Status extends ImmutablePureComponent { hidden, unread, featured, - usingPiP, + pictureInPicture, ...other } = this.props; const { isCollapsed, forceFilter } = this.state; @@ -595,7 +598,7 @@ class Status extends ImmutablePureComponent { attachments = status.get('media_attachments'); - if (usingPiP) { + if (pictureInPicture.inUse) { media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />); mediaIcons.push('video-camera'); } else if (attachments.size > 0) { @@ -623,7 +626,7 @@ class Status extends ImmutablePureComponent { width={this.props.cachedMediaWidth} height={110} cacheWidth={this.props.cacheMediaWidth} - deployPictureInPicture={this.handleDeployPictureInPicture} + deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} sensitive={status.get('sensitive')} blurhash={attachment.get('blurhash')} visible={this.state.showMedia} @@ -652,7 +655,7 @@ class Status extends ImmutablePureComponent { onOpenVideo={this.handleOpenVideo} width={this.props.cachedMediaWidth} cacheWidth={this.props.cacheMediaWidth} - deployPictureInPicture={this.handleDeployPictureInPicture} + deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} />)} @@ -709,20 +712,31 @@ class Status extends ImmutablePureComponent { 'data-status-by': `@${status.getIn(['account', 'acct'])}`, }; - if (prepend && account) { + let prepend; + + if (this.props.prepend && account) { const notifKind = { favourite: 'favourited', reblog: 'boosted', reblogged_by: 'boosted', status: 'posted', - }[prepend]; + }[this.props.prepend]; selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`; + + prepend = ( + <StatusPrepend + type={this.props.prepend} + account={account} + parseClick={parseClick} + notificationId={this.props.notificationId} + /> + ); } let rebloggedByText; - if (prepend === 'reblog') { + if (this.props.prepend === 'reblog') { rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') }); } @@ -745,16 +759,10 @@ class Status extends ImmutablePureComponent { data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} > + {!muted && prepend} <header className='status__info'> <span> - {prepend && account ? ( - <StatusPrepend - type={prepend} - account={account} - parseClick={parseClick} - notificationId={this.props.notificationId} - /> - ) : null} + {muted && prepend} {!muted || !isCollapsed ? ( <StatusHeader status={status} diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index c0cd496ce..977c98ccb 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -5,9 +5,9 @@ import IconButton from './icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me } from 'flavours/glitch/initial_state'; import RelativeTimestamp from './relative_timestamp'; -import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_links'; +import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import classNames from 'classnames'; import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; @@ -42,6 +42,7 @@ const messages = defineMessages({ hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, filter: { id: 'status.filter', defaultMessage: 'Filter this post' }, + openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, }); export default @injectIntl @@ -69,7 +70,9 @@ class StatusActionBar extends ImmutablePureComponent { onBookmark: PropTypes.func, onFilter: PropTypes.func, onAddFilter: PropTypes.func, + onInteractionModal: PropTypes.func, withDismiss: PropTypes.bool, + withCounters: PropTypes.bool, showReplyCount: PropTypes.bool, scrollKey: PropTypes.string, intl: PropTypes.object.isRequired, @@ -80,14 +83,17 @@ class StatusActionBar extends ImmutablePureComponent { updateOnProps = [ 'status', 'showReplyCount', + 'withCounters', 'withDismiss', ] handleReplyClick = () => { - if (me) { + const { signedIn } = this.context.identity; + + if (signedIn) { this.props.onReply(this.props.status, this.context.router.history); } else { - this._openInteractionDialog('reply'); + this.props.onInteractionModal('reply', this.props.status); } } @@ -99,28 +105,28 @@ class StatusActionBar extends ImmutablePureComponent { } handleFavouriteClick = (e) => { - if (me) { + const { signedIn } = this.context.identity; + + if (signedIn) { this.props.onFavourite(this.props.status, e); } else { - this._openInteractionDialog('favourite'); + this.props.onInteractionModal('favourite', this.props.status); } } - handleBookmarkClick = (e) => { - this.props.onBookmark(this.props.status, e); - } - handleReblogClick = e => { - if (me) { + const { signedIn } = this.context.identity; + + if (signedIn) { this.props.onReblog(this.props.status, e); } else { - this._openInteractionDialog('reblog'); + this.props.onInteractionModal('reblog', this.props.status); } } - _openInteractionDialog = type => { - window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - } + handleBookmarkClick = (e) => { + this.props.onBookmark(this.props.status, e); + } handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); @@ -177,22 +183,8 @@ class StatusActionBar extends ImmutablePureComponent { } handleCopy = () => { - const url = this.props.status.get('url'); - const textarea = document.createElement('textarea'); - - textarea.textContent = url; - textarea.style.position = 'fixed'; - - document.body.appendChild(textarea); - - try { - textarea.select(); - document.execCommand('copy'); - } catch (e) { - - } finally { - document.body.removeChild(textarea); - } + const url = this.props.status.get('url'); + navigator.clipboard.writeText(url); } handleHideClick = () => { @@ -204,13 +196,14 @@ class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss, showReplyCount, scrollKey } = this.props; + const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props; const anonymousAccess = !me; const mutingConversation = status.get('muted'); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const writtenByMe = status.getIn(['account', 'id']) === me; + const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); let menu = []; let reblogIcon = 'retweet'; @@ -220,6 +213,9 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen }); if (publicStatus) { + if (isRemote) { + menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); + } menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } @@ -237,7 +233,7 @@ class StatusActionBar extends ImmutablePureComponent { } if (writtenByMe) { - // menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); } else { @@ -283,27 +279,6 @@ class StatusActionBar extends ImmutablePureComponent { <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> ); - let replyButton = ( - <IconButton - className='status__action-bar-button' - title={replyTitle} - icon={replyIcon} - onClick={this.handleReplyClick} - /> - ); - if (showReplyCount) { - replyButton = ( - <IconButton - className='status__action-bar-button' - title={replyTitle} - icon={replyIcon} - onClick={this.handleReplyClick} - counter={status.get('replies_count')} - obfuscateCount - /> - ); - } - const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; let reblogTitle = ''; @@ -323,11 +298,18 @@ class StatusActionBar extends ImmutablePureComponent { return ( <div className='status__action-bar'> - {replyButton} - <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} /> - <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> + <IconButton + className='status__action-bar-button' + title={replyTitle} + icon={replyIcon} + onClick={this.handleReplyClick} + counter={showReplyCount ? status.get('replies_count') : undefined} + obfuscateCount + /> + <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> + <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> {shareButton} - <IconButton 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} /> + <IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /> {filterButton} diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 39891da4f..c618cedca 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -5,8 +5,8 @@ import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; -import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; +import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; const textMatchesTarget = (text, origin, host) => { return (text === origin || text === host @@ -124,6 +124,9 @@ export default class StatusContent extends React.PureComponent { link.setAttribute('title', link.href); link.classList.add('unhandled-link'); + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener nofollow noreferrer'); + try { if (tagLinks && isLinkMisleading(link)) { // Add a tag besides the link to display its origin @@ -149,9 +152,6 @@ export default class StatusContent extends React.PureComponent { if (tagLinks && e instanceof TypeError) link.removeAttribute('href'); } } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer'); } } @@ -332,7 +332,7 @@ export default class StatusContent extends React.PureComponent { > <span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={lang} /> {' '} - <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> + <button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}> {toggleText} </button> </p> diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js index 2226aaef2..71ffb2e56 100644 --- a/app/javascript/flavours/glitch/components/status_icons.js +++ b/app/javascript/flavours/glitch/components/status_icons.js @@ -8,7 +8,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import IconButton from './icon_button'; import VisibilityIcon from './status_visibility_icon'; import Icon from 'flavours/glitch/components/icon'; -import { languages } from 'flavours/glitch/util/initial_state'; +import { languages } from 'flavours/glitch/initial_state'; // Messages for use with internationalization stuff. const messages = defineMessages({ diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index 9095e087e..0d843a27d 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -22,9 +22,11 @@ export default class StatusList extends ImmutablePureComponent { isPartial: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, - alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, + alwaysPrepend: PropTypes.bool, + withCounters: PropTypes.bool, timelineId: PropTypes.string.isRequired, + regex: PropTypes.string, }; static defaultProps = { @@ -99,6 +101,7 @@ export default class StatusList extends ImmutablePureComponent { onMoveDown={this.handleMoveDown} contextType={timelineId} scrollKey={this.props.scrollKey} + withCounters={this.props.withCounters} /> )) ) : null; @@ -113,6 +116,7 @@ export default class StatusList extends ImmutablePureComponent { onMoveDown={this.handleMoveDown} contextType={timelineId} scrollKey={this.props.scrollKey} + withCounters={this.props.withCounters} /> )).concat(scrollableContent); } diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index d85009362..f82533062 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import Icon from 'flavours/glitch/components/icon'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me } from 'flavours/glitch/initial_state'; export default class StatusPrepend extends React.PureComponent { |