diff options
Diffstat (limited to 'app/javascript/flavours/glitch/components')
23 files changed, 811 insertions, 179 deletions
diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js index 20313535b..396a36ea0 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.js @@ -128,7 +128,7 @@ class Account extends ImmutablePureComponent { <Permalink className='account small' href={account.get('url')} - to={`/accounts/${account.get('id')}`} + to={`/@${account.get('acct')}`} > <div className='account__avatar-wrapper'> <Avatar @@ -144,7 +144,7 @@ class Account extends ImmutablePureComponent { ) : ( <div className='account'> <div className='account__wrapper'> - <Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <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> {mute_expires_at} <DisplayName account={account} /> diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js new file mode 100644 index 000000000..2bc9ce482 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Counter.js @@ -0,0 +1,116 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedNumber } from 'react-intl'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; +import classNames from 'classnames'; +import Skeleton from 'flavours/glitch/components/skeleton'; + +const percIncrease = (a, b) => { + let percent; + + if (b !== 0) { + if (a !== 0) { + percent = (b - a) / a; + } else { + percent = 1; + } + } else if (b === 0 && a === 0) { + percent = 0; + } else { + percent = - 1; + } + + return percent; +}; + +export default class Counter extends React.PureComponent { + + static propTypes = { + measure: PropTypes.string.isRequired, + start_at: PropTypes.string.isRequired, + end_at: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + href: PropTypes.string, + params: PropTypes.object, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { measure, start_at, end_at, params } = this.props; + + api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { label, href } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + <React.Fragment> + <span className='sparkline__value__total'><Skeleton width={43} /></span> + <span className='sparkline__value__change'><Skeleton width={43} /></span> + </React.Fragment> + ); + } else { + const measure = data[0]; + const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1); + + content = ( + <React.Fragment> + <span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span> + <span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span> + </React.Fragment> + ); + } + + const inner = ( + <React.Fragment> + <div className='sparkline__value'> + {content} + </div> + + <div className='sparkline__label'> + {label} + </div> + + <div className='sparkline__graph'> + {!loading && ( + <Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}> + <SparklinesCurve /> + </Sparklines> + )} + </div> + </React.Fragment> + ); + + if (href) { + return ( + <a href={href} className='sparkline'> + {inner} + </a> + ); + } else { + return ( + <div className='sparkline'> + {inner} + </div> + ); + } + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.js new file mode 100644 index 000000000..a924d093c --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Dimension.js @@ -0,0 +1,93 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedNumber } from 'react-intl'; +import { roundTo10 } from 'flavours/glitch/util/numbers'; +import Skeleton from 'flavours/glitch/components/skeleton'; + +export default class Dimension extends React.PureComponent { + + static propTypes = { + dimension: PropTypes.string.isRequired, + start_at: PropTypes.string.isRequired, + end_at: PropTypes.string.isRequired, + limit: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + params: PropTypes.object, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { start_at, end_at, dimension, limit, params } = this.props; + + api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { label, limit } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + <table> + <tbody> + {Array.from(Array(limit)).map((_, i) => ( + <tr className='dimension__item' key={i}> + <td className='dimension__item__key'> + <Skeleton width={100} /> + </td> + + <td className='dimension__item__value'> + <Skeleton width={60} /> + </td> + </tr> + ))} + </tbody> + </table> + ); + } else { + const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0); + + content = ( + <table> + <tbody> + {data[0].data.map(item => ( + <tr className='dimension__item' key={item.key}> + <td className='dimension__item__key'> + <span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} /> + <span title={item.key}>{item.human_key}</span> + </td> + + <td className='dimension__item__value'> + {typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />} + </td> + </tr> + ))} + </tbody> + </table> + ); + } + + return ( + <div className='dimension'> + <h4>{label}</h4> + + {content} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js new file mode 100644 index 000000000..0f2a4fe36 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js @@ -0,0 +1,159 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { injectIntl, defineMessages } from 'react-intl'; +import classNames from 'classnames'; + +const messages = defineMessages({ + other: { id: 'report.categories.other', defaultMessage: 'Other' }, + spam: { id: 'report.categories.spam', defaultMessage: 'Spam' }, + violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, +}); + +class Category extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool, + onSelect: PropTypes.func, + children: PropTypes.node, + }; + + handleClick = () => { + const { id, disabled, onSelect } = this.props; + + if (!disabled) { + onSelect(id); + } + }; + + render () { + const { id, text, disabled, selected, children } = this.props; + + return ( + <div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}> + {selected && <input type='hidden' name='report[category]' value={id} />} + + <div className='report-reason-selector__category__label'> + <span className={classNames('poll__input', { active: selected, disabled })} /> + {text} + </div> + + {(selected && children) && ( + <div className='report-reason-selector__category__rules'> + {children} + </div> + )} + </div> + ); + } + +} + +class Rule extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + text: PropTypes.string.isRequired, + selected: PropTypes.bool, + disabled: PropTypes.bool, + onToggle: PropTypes.func, + }; + + handleClick = () => { + const { id, disabled, onToggle } = this.props; + + if (!disabled) { + onToggle(id); + } + }; + + render () { + const { id, text, disabled, selected } = this.props; + + return ( + <div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}> + <span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} /> + {selected && <input type='hidden' name='report[rule_ids][]' value={id} />} + {text} + </div> + ); + } + +} + +export default @injectIntl +class ReportReasonSelector extends React.PureComponent { + + static propTypes = { + id: PropTypes.string.isRequired, + category: PropTypes.string.isRequired, + rule_ids: PropTypes.arrayOf(PropTypes.string), + disabled: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + category: this.props.category, + rule_ids: this.props.rule_ids || [], + rules: [], + }; + + componentDidMount() { + api().get('/api/v1/instance').then(res => { + this.setState({ + rules: res.data.rules, + }); + }).catch(err => { + console.error(err); + }); + } + + _save = () => { + const { id, disabled } = this.props; + const { category, rule_ids } = this.state; + + if (disabled) { + return; + } + + api().put(`/api/v1/admin/reports/${id}`, { + category, + rule_ids, + }).catch(err => { + console.error(err); + }); + }; + + handleSelect = id => { + this.setState({ category: id }, () => this._save()); + }; + + handleToggle = id => { + const { rule_ids } = this.state; + + if (rule_ids.includes(id)) { + this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save()); + } else { + this.setState({ rule_ids: [...rule_ids, id] }, () => this._save()); + } + }; + + render () { + const { disabled, intl } = this.props; + const { rules, category, rule_ids } = this.state; + + return ( + <div className='report-reason-selector'> + <Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} /> + <Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} /> + <Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}> + {rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)} + </Category> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Retention.js b/app/javascript/flavours/glitch/components/admin/Retention.js new file mode 100644 index 000000000..6d7e4b279 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Retention.js @@ -0,0 +1,151 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; +import classNames from 'classnames'; +import { roundTo10 } from 'flavours/glitch/util/numbers'; + +const dateForCohort = cohort => { + switch(cohort.frequency) { + case 'day': + return <FormattedDate value={cohort.period} month='long' day='2-digit' />; + default: + return <FormattedDate value={cohort.period} month='long' year='numeric' />; + } +}; + +export default class Retention extends React.PureComponent { + + static propTypes = { + start_at: PropTypes.string, + end_at: PropTypes.string, + frequency: PropTypes.string, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { start_at, end_at, frequency } = this.props; + + api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + const { frequency } = this.props; + + let content; + + if (loading) { + content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />; + } else { + content = ( + <table className='retention__table'> + <thead> + <tr> + <th> + <div className='retention__table__date retention__table__label'> + <FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' /> + </div> + </th> + + <th> + <div className='retention__table__number retention__table__label'> + <FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' /> + </div> + </th> + + {data[0].data.slice(1).map((retention, i) => ( + <th key={retention.date}> + <div className='retention__table__number retention__table__label'> + {i + 1} + </div> + </th> + ))} + </tr> + + <tr> + <td> + <div className='retention__table__date retention__table__average'> + <FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' /> + </div> + </td> + + <td> + <div className='retention__table__size'> + <FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} /> + </div> + </td> + + {data[0].data.slice(1).map((retention, i) => { + const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0); + + return ( + <td key={retention.date}> + <div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}> + <FormattedNumber value={average} style='percent' /> + </div> + </td> + ); + })} + </tr> + </thead> + + <tbody> + {data.slice(0, -1).map(cohort => ( + <tr key={cohort.period}> + <td> + <div className='retention__table__date'> + {dateForCohort(cohort)} + </div> + </td> + + <td> + <div className='retention__table__size'> + <FormattedNumber value={cohort.data[0].value} /> + </div> + </td> + + {cohort.data.slice(1).map(retention => ( + <td key={retention.date}> + <div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}> + <FormattedNumber value={retention.rate} style='percent' /> + </div> + </td> + ))} + </tr> + ))} + </tbody> + </table> + ); + } + + let title = null; + switch(frequency) { + case 'day': + title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />; + break; + default: + title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />; + }; + + return ( + <div className='retention'> + <h4>{title}</h4> + + {content} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.js new file mode 100644 index 000000000..60e367f00 --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/Trends.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import api from 'flavours/glitch/util/api'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +import Hashtag from 'flavours/glitch/components/hashtag'; + +export default class Trends extends React.PureComponent { + + static propTypes = { + limit: PropTypes.number.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { limit } = this.props; + + api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { limit } = this.props; + const { loading, data } = this.state; + + let content; + + if (loading) { + content = ( + <div> + {Array.from(Array(limit)).map((_, i) => ( + <Hashtag key={i} /> + ))} + </div> + ); + } else { + content = ( + <div> + {data.map(hashtag => ( + <Hashtag + key={hashtag.name} + name={hashtag.name} + href={`/admin/tags/${hashtag.id}`} + people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1} + uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1} + history={hashtag.history.reverse().map(day => day.uses)} + className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')} + /> + ))} + </div> + ); + } + + return ( + <div className='trends trends--compact'> + <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4> + + {content} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.js index 68d8d29c7..68b80b19f 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.js +++ b/app/javascript/flavours/glitch/components/attachment_list.js @@ -2,6 +2,8 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; @@ -16,29 +18,13 @@ export default class AttachmentList extends ImmutablePureComponent { render () { const { media, compact } = this.props; - if (compact) { - return ( - <div className='attachment-list compact'> - <ul className='attachment-list__list'> - {media.map(attachment => { - const displayUrl = attachment.get('remote_url') || attachment.get('url'); - - return ( - <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener noreferrer'><Icon id='link' /> {filename(displayUrl)}</a> - </li> - ); - })} - </ul> - </div> - ); - } - return ( - <div className='attachment-list'> - <div className='attachment-list__icon'> - <Icon id='link' /> - </div> + <div className={classNames('attachment-list', { compact })}> + {!compact && ( + <div className='attachment-list__icon'> + <Icon id='link' /> + </div> + )} <ul className='attachment-list__list'> {media.map(attachment => { @@ -46,7 +32,11 @@ export default class AttachmentList extends ImmutablePureComponent { return ( <li key={attachment.get('id')}> - <a href={displayUrl} target='_blank' rel='noopener noreferrer'>{filename(displayUrl)}</a> + <a href={displayUrl} target='_blank' rel='noopener noreferrer'> + {compact && <Icon id='link' />} + {compact && ' ' } + {displayUrl ? filename(displayUrl) : <FormattedMessage id='attachments_list.unprocessed' defaultMessage='(unprocessed)' />} + </a> </li> ); })} diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.js index 125b51c44..e30dfe68a 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.js +++ b/app/javascript/flavours/glitch/components/avatar_composite.js @@ -82,7 +82,7 @@ export default class AvatarComposite extends React.PureComponent { <a href={account.get('url')} target='_blank' - onClick={(e) => this.props.onAccountClick(account.get('id'), e)} + onClick={(e) => this.props.onAccountClick(account.get('acct'), e)} title={`@${account.get('acct')}`} key={account.get('id')} > diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.js index ccd0714f1..500612093 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.js @@ -124,8 +124,8 @@ class ColumnHeader extends React.PureComponent { moveButtons = ( <div key='move-buttons' className='column-header__setting-arrows'> - <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> - <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> + <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button> + <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button> </div> ); } else if (multiColumn && this.props.onPin) { @@ -146,8 +146,8 @@ class ColumnHeader extends React.PureComponent { ]; if (multiColumn) { - collapsedContent.push(moveButtons); collapsedContent.push(pinButton); + collapsedContent.push(moveButtons); } if (children || (multiColumn && this.props.onPin)) { diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js index ad978a2c6..9c7da744e 100644 --- a/app/javascript/flavours/glitch/components/display_name.js +++ b/app/javascript/flavours/glitch/components/display_name.js @@ -61,7 +61,7 @@ export default class DisplayName extends React.PureComponent { <a href={a.get('url')} target='_blank' - onClick={(e) => onAccountClick(a.get('id'), e)} + onClick={(e) => onAccountClick(a.get('acct'), e)} title={`@${a.get('acct')}`} rel='noopener noreferrer' > @@ -76,7 +76,7 @@ export default class DisplayName extends React.PureComponent { } suffix = ( - <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('id'), e)} rel='noopener noreferrer'> + <a href={account.get('url')} target='_blank' onClick={(e) => onAccountClick(account.get('acct'), e)} rel='noopener noreferrer'> <span className='display-name__account'>@{acct}</span> </a> ); diff --git a/app/javascript/flavours/glitch/components/error_boundary.js b/app/javascript/flavours/glitch/components/error_boundary.js index 8e6cd1461..4537bde1d 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.js +++ b/app/javascript/flavours/glitch/components/error_boundary.js @@ -1,6 +1,7 @@ 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 StackTrace from 'stacktrace-js'; @@ -64,6 +65,11 @@ export default class ErrorBoundary extends React.PureComponent { debugInfo += 'React component stack\n---------------------\n\n```\n' + componentStack.toString() + '\n```'; } + let issueTracker = source_url; + if (source_url.match(/^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/)) { + issueTracker = source_url + '/issues'; + } + return ( <div tabIndex='-1'> <div className='error-boundary'> @@ -84,7 +90,7 @@ export default class ErrorBoundary extends React.PureComponent { <FormattedMessage id='web_app_crash.report_issue' defaultMessage='Report a bug in the {issuetracker}' - values={{ issuetracker: <a href='https://github.com/glitch-soc/mastodon/issues' rel='noopener noreferrer' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }} + values={{ issuetracker: <a href={issueTracker} rel='noopener noreferrer' target='_blank'><FormattedMessage id='web_app_crash.issue_tracker' defaultMessage='issue tracker' /></a> }} /> { debugInfo !== '' && ( <details> diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 24c595ed7..769185a2b 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -6,6 +6,8 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from './permalink'; import ShortNumber from 'flavours/glitch/components/short_number'; +import Skeleton from 'flavours/glitch/components/skeleton'; +import classNames from 'classnames'; class SilentErrorBoundary extends React.Component { @@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); -const Hashtag = ({ hashtag }) => ( - <div className='trends__item'> +export const ImmutableHashtag = ({ hashtag }) => ( + <Hashtag + name={hashtag.get('name')} + 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()} + /> +); + +ImmutableHashtag.propTypes = { + hashtag: ImmutablePropTypes.map.isRequired, +}; + +const Hashtag = ({ name, href, to, people, uses, history, className }) => ( + <div className={classNames('trends__item', className)}> <div className='trends__item__name'> - <Permalink - href={hashtag.get('url')} - to={`/timelines/tag/${hashtag.get('name')}`} - > - #<span>{hashtag.get('name')}</span> + <Permalink href={href} to={to}> + {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />} </Permalink> - <ShortNumber - value={ - hashtag.getIn(['history', 0, 'accounts']) * 1 + - hashtag.getIn(['history', 1, 'accounts']) * 1 - } - renderer={accountsCountRenderer} - /> + {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />} </div> <div className='trends__item__current'> - <ShortNumber - value={ - hashtag.getIn(['history', 0, 'uses']) * 1 + - hashtag.getIn(['history', 1, 'uses']) * 1 - } - /> + {typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />} </div> <div className='trends__item__sparkline'> <SilentErrorBoundary> - <Sparklines - width={50} - height={28} - data={hashtag - .get('history') - .reverse() - .map((day) => day.get('uses')) - .toArray()} - > + <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}> <SparklinesCurve style={{ fill: 'none' }} /> </Sparklines> </SilentErrorBoundary> @@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => ( ); Hashtag.propTypes = { - hashtag: ImmutablePropTypes.map.isRequired, + name: PropTypes.string, + href: PropTypes.string, + to: PropTypes.string, + people: PropTypes.number, + uses: PropTypes.number, + history: PropTypes.arrayOf(PropTypes.number), + className: PropTypes.string, }; export default Hashtag; diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 913234d32..7b5a630e5 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -76,10 +76,13 @@ export default class ModalRoot extends React.PureComponent { this.activeElement = null; }).catch(console.error); - this.handleModalClose(); + this._handleModalClose(); } if (this.props.children && !prevProps.children) { - this.handleModalOpen(); + this._handleModalOpen(); + } + if (this.props.children) { + this._ensureHistoryBuffer(); } } @@ -88,22 +91,29 @@ export default class ModalRoot extends React.PureComponent { window.removeEventListener('keydown', this.handleKeyDown); } - handleModalClose () { + _handleModalOpen () { + this._modalHistoryKey = Date.now(); + this.unlistenHistory = this.history.listen((_, action) => { + if (action === 'POP') { + this.props.onClose(); + } + }); + } + + _handleModalClose () { this.unlistenHistory(); - const state = this.history.location.state; - if (state && state.mastodonModalOpen) { + const { state } = this.history.location; + if (state && state.mastodonModalKey === this._modalHistoryKey) { this.history.goBack(); } } - handleModalOpen () { - const history = this.history; - const state = {...history.location.state, mastodonModalOpen: true}; - history.push(history.location.pathname, state); - this.unlistenHistory = history.listen(() => { - this.props.onClose(); - }); + _ensureHistoryBuffer () { + const { pathname, state } = this.history.location; + if (!state || state.mastodonModalKey !== this._modalHistoryKey) { + this.history.push(pathname, { ...state, mastodonModalKey: this._modalHistoryKey }); + } } getSiblings = () => { diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.js index f230823cc..970b00705 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.js @@ -12,8 +12,18 @@ import RelativeTimestamp from './relative_timestamp'; import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ - closed: { id: 'poll.closed', defaultMessage: 'Closed' }, - voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' }, + closed: { + id: 'poll.closed', + defaultMessage: 'Closed', + }, + voted: { + id: 'poll.voted', + defaultMessage: 'You voted for this answer', + }, + votes: { + id: 'poll.votes', + defaultMessage: '{votes, plural, one {# vote} other {# votes}}', + }, }); const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { @@ -148,9 +158,16 @@ class Poll extends ImmutablePureComponent { data-index={optionIndex} /> )} - {showResults && <span className='poll__number'> - {Math.round(percent)}% - </span>} + {showResults && ( + <span + className='poll__number' + title={intl.formatMessage(messages.votes, { + votes: option.get('votes_count'), + })} + > + {Math.round(percent)}% + </span> + )} <span className='poll__option__text translate' diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index cc8d9f1f3..16f13afa4 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { ScrollContainer } from 'react-router-scroll-4'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import LoadMore from './load_more'; @@ -34,7 +34,6 @@ class ScrollableList extends PureComponent { onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, - shouldUpdateScroll: PropTypes.func, isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, @@ -264,11 +263,6 @@ class ScrollableList extends PureComponent { this.props.onLoadMore(); } - defaultShouldUpdateScroll = (prevRouterProps, { location }) => { - if ((((prevRouterProps || {}).location || {}).state || {}).mastodonModalOpen) return false; - return !(location.state && location.state.mastodonModalOpen); - } - handleLoadPending = e => { e.preventDefault(); this.props.onLoadPending(); @@ -282,7 +276,7 @@ class ScrollableList extends PureComponent { } render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); @@ -348,7 +342,7 @@ class ScrollableList extends PureComponent { if (trackScroll) { return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}> + <ScrollContainer scrollKey={scrollKey}> {scrollableArea} </ScrollContainer> ); diff --git a/app/javascript/flavours/glitch/components/skeleton.js b/app/javascript/flavours/glitch/components/skeleton.js new file mode 100644 index 000000000..09093e99c --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>; + +Skeleton.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default Skeleton; diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 782fd918e..02ff9ab28 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -346,7 +346,9 @@ class Status extends ImmutablePureComponent { return; } else { if (destination === undefined) { - destination = `/statuses/${ + destination = `/@${ + status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct'])) + }/${ status.getIn(['reblog', 'id'], status.get('id')) }`; } @@ -362,16 +364,6 @@ class Status extends ImmutablePureComponent { this.setState({ showMedia: !this.state.showMedia }); } - handleAccountClick = (e) => { - if (this.context.router && e.button === 0) { - const id = e.currentTarget.getAttribute('data-id'); - e.preventDefault(); - let state = {...this.context.router.history.location.state}; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/accounts/${id}`, state); - } - } - handleExpandedToggle = () => { if (this.props.status.get('spoiler_text')) { this.setExpansion(!this.state.isExpanded); @@ -433,13 +425,14 @@ class Status extends ImmutablePureComponent { handleHotkeyOpen = () => { let state = {...this.context.router.history.location.state}; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state); + const status = this.props.status; + this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state); } handleHotkeyOpenProfile = () => { let state = {...this.context.router.history.location.state}; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`, state); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state); } handleHotkeyMoveUp = e => { @@ -516,8 +509,8 @@ class Status extends ImmutablePureComponent { const { isExpanded, isCollapsed, forceFilter } = this.state; let background = null; let attachments = null; - let media = null; - let mediaIcon = null; + let media = []; + let mediaIcons = []; if (status === null) { return null; @@ -543,9 +536,8 @@ class Status extends ImmutablePureComponent { return ( <HotKeys handlers={handlers}> <div ref={this.handleRef} className='status focusable' tabIndex='0'> - {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {' '} - {status.get('content')} + <span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span> + <span>{status.get('content')}</span> </div> </HotKeys> ); @@ -587,25 +579,27 @@ class Status extends ImmutablePureComponent { // After we have generated our appropriate media element and stored it in // `media`, we snatch the thumbnail to use as our `background` if media // backgrounds for collapsed statuses are enabled. + attachments = status.get('media_attachments'); if (status.get('poll')) { - media = <PollContainer pollId={status.get('poll')} />; - mediaIcon = 'tasks'; - } else if (usingPiP) { - media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; - mediaIcon = 'video-camera'; + media.push(<PollContainer pollId={status.get('poll')} />); + mediaIcons.push('tasks'); + } + if (usingPiP) { + media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />); + mediaIcons.push('video-camera'); } else if (attachments.size > 0) { if (muted || attachments.some(item => item.get('type') === 'unknown')) { - media = ( + media.push( <AttachmentList compact media={status.get('media_attachments')} - /> + />, ); } else if (attachments.getIn([0, 'type']) === 'audio') { const attachment = status.getIn(['media_attachments', 0]); - media = ( + media.push( <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > {Component => ( <Component @@ -622,13 +616,13 @@ class Status extends ImmutablePureComponent { deployPictureInPicture={this.handleDeployPictureInPicture} /> )} - </Bundle> + </Bundle>, ); - mediaIcon = 'music'; + mediaIcons.push('music'); } else if (attachments.getIn([0, 'type']) === 'video') { const attachment = status.getIn(['media_attachments', 0]); - media = ( + media.push( <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => (<Component preview={attachment.get('preview_url')} @@ -648,11 +642,11 @@ class Status extends ImmutablePureComponent { visible={this.state.showMedia} onToggleVisibility={this.handleToggleMediaVisibility} />)} - </Bundle> + </Bundle>, ); - mediaIcon = 'video-camera'; + mediaIcons.push('video-camera'); } else { // Media type is 'image' or 'gifv' - media = ( + media.push( <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> {Component => ( <Component @@ -668,16 +662,16 @@ class Status extends ImmutablePureComponent { onToggleVisibility={this.handleToggleMediaVisibility} /> )} - </Bundle> + </Bundle>, ); - mediaIcon = 'picture-o'; + mediaIcons.push('picture-o'); } if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) { background = attachments.getIn([0, 'preview_url']); } } else if (status.get('card') && settings.get('inline_preview_cards')) { - media = ( + media.push( <Card onOpenMedia={this.handleOpenMedia} card={status.get('card')} @@ -685,9 +679,9 @@ class Status extends ImmutablePureComponent { cacheWidth={this.props.cacheMediaWidth} defaultWidth={this.props.cachedMediaWidth} sensitive={status.get('sensitive')} - /> + />, ); - mediaIcon = 'link'; + mediaIcons.push('link'); } // Here we prepare extra data-* attributes for CSS selectors. @@ -754,7 +748,7 @@ class Status extends ImmutablePureComponent { </span> <StatusIcons status={status} - mediaIcon={mediaIcon} + mediaIcons={mediaIcons} collapsible={settings.getIn(['collapsed', 'enabled'])} collapsed={isCollapsed} setCollapsed={setCollapsed} @@ -764,7 +758,7 @@ class Status extends ImmutablePureComponent { <StatusContent status={status} media={media} - mediaIcon={mediaIcon} + mediaIcons={mediaIcons} expanded={isExpanded} onExpandedToggle={this.handleExpandedToggle} parseClick={parseClick} diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 74bfd948e..ae67c6116 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -38,6 +38,7 @@ const messages = defineMessages({ admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, hide: { id: 'status.hide', defaultMessage: 'Hide toot' }, + edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, }); export default @injectIntl @@ -147,11 +148,11 @@ class StatusActionBar extends ImmutablePureComponent { handleOpen = () => { let state = {...this.context.router.history.location.state}; - if (state.mastodonModalOpen) { - this.context.router.history.replace(`/statuses/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 }); + if (state.mastodonModalKey) { + this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 }); } else { state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`, state); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, state); } } @@ -196,6 +197,7 @@ class StatusActionBar extends ImmutablePureComponent { 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; let menu = []; @@ -212,7 +214,7 @@ class StatusActionBar extends ImmutablePureComponent { menu.push(null); - if (writtenByMe && publicStatus) { + if (writtenByMe && pinnableStatus) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push(null); } @@ -323,7 +325,9 @@ class StatusActionBar extends ImmutablePureComponent { </div>, ]} - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a> + <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'> + <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>} + </a> </div> ); } diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 34ff97305..1d32b35e5 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -69,8 +69,8 @@ export default class StatusContent extends React.PureComponent { expanded: PropTypes.bool, collapsed: PropTypes.bool, onExpandedToggle: PropTypes.func, - media: PropTypes.element, - mediaIcon: PropTypes.string, + media: PropTypes.node, + mediaIcons: PropTypes.arrayOf(PropTypes.string), parseClick: PropTypes.func, disabled: PropTypes.bool, onUpdate: PropTypes.func, @@ -197,7 +197,7 @@ export default class StatusContent extends React.PureComponent { onMentionClick = (mention, e) => { if (this.props.parseClick) { - this.props.parseClick(e, `/accounts/${mention.get('id')}`); + this.props.parseClick(e, `/@${mention.get('acct')}`); } } @@ -205,7 +205,7 @@ export default class StatusContent extends React.PureComponent { hashtag = hashtag.replace(/^#/, ''); if (this.props.parseClick) { - this.props.parseClick(e, `/timelines/tag/${hashtag}`); + this.props.parseClick(e, `/tags/${hashtag}`); } } @@ -224,8 +224,8 @@ export default class StatusContent extends React.PureComponent { const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)]; let element = e.target; - while (element) { - if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) { + while (element !== e.currentTarget) { + if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName) || element.getAttribute('role') === 'button') { return; } element = element.parentNode; @@ -256,7 +256,7 @@ export default class StatusContent extends React.PureComponent { const { status, media, - mediaIcon, + mediaIcons, parseClick, disabled, tagLinks, @@ -277,7 +277,7 @@ export default class StatusContent extends React.PureComponent { const mentionLinks = status.get('mentions').map(item => ( <Permalink - to={`/accounts/${item.get('id')}`} + to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='mention' @@ -286,28 +286,37 @@ export default class StatusContent extends React.PureComponent { </Permalink> )).reduce((aggregate, item) => [...aggregate, item, ' '], []); - const toggleText = hidden ? [ - <FormattedMessage - id='status.show_more' - defaultMessage='Show more' - key='0' - />, - mediaIcon ? ( - <Icon - fixedWidth - className='status__content__spoiler-icon' - id={mediaIcon} - aria-hidden='true' - key='1' + let toggleText = null; + if (hidden) { + toggleText = [ + <FormattedMessage + id='status.show_more' + defaultMessage='Show more' + key='0' + />, + ]; + if (mediaIcons) { + mediaIcons.forEach((mediaIcon, idx) => { + toggleText.push( + <Icon + fixedWidth + className='status__content__spoiler-icon' + id={mediaIcon} + aria-hidden='true' + key={`icon-${idx}`} + />, + ); + }); + } + } else { + toggleText = ( + <FormattedMessage + id='status.show_less' + defaultMessage='Show less' + key='0' /> - ) : null, - ] : [ - <FormattedMessage - id='status.show_less' - defaultMessage='Show less' - key='0' - />, - ]; + ); + } if (hidden) { mentionsPlaceholder = <div>{mentionLinks}</div>; diff --git a/app/javascript/flavours/glitch/components/status_header.js b/app/javascript/flavours/glitch/components/status_header.js index 06296e124..cc476139b 100644 --- a/app/javascript/flavours/glitch/components/status_header.js +++ b/app/javascript/flavours/glitch/components/status_header.js @@ -19,14 +19,14 @@ export default class StatusHeader extends React.PureComponent { }; // Handles clicks on account name/image - handleClick = (id, e) => { + handleClick = (acct, e) => { const { parseClick } = this.props; - parseClick(e, `/accounts/${id}`); + parseClick(e, `/@${acct}`); } handleAccountClick = (e) => { const { status } = this.props; - this.handleClick(status.getIn(['account', 'id']), e); + this.handleClick(status.getIn(['account', 'acct']), e); } // Rendering. diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js index f4d0a7405..e66947f4a 100644 --- a/app/javascript/flavours/glitch/components/status_icons.js +++ b/app/javascript/flavours/glitch/components/status_icons.js @@ -27,7 +27,7 @@ class StatusIcons extends React.PureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, - mediaIcon: PropTypes.string, + mediaIcons: PropTypes.arrayOf(PropTypes.string), collapsible: PropTypes.bool, collapsed: PropTypes.bool, directMessage: PropTypes.bool, @@ -44,8 +44,8 @@ class StatusIcons extends React.PureComponent { } } - mediaIconTitleText () { - const { intl, mediaIcon } = this.props; + mediaIconTitleText (mediaIcon) { + const { intl } = this.props; switch (mediaIcon) { case 'link': @@ -61,11 +61,24 @@ class StatusIcons extends React.PureComponent { } } + renderIcon (mediaIcon) { + return ( + <Icon + fixedWidth + className='status__media-icon' + key={`media-icon--${mediaIcon}`} + id={mediaIcon} + aria-hidden='true' + title={this.mediaIconTitleText(mediaIcon)} + /> + ); + } + // Rendering. render () { const { status, - mediaIcon, + mediaIcons, collapsible, collapsed, directMessage, @@ -90,15 +103,7 @@ class StatusIcons extends React.PureComponent { aria-hidden='true' title={intl.formatMessage(messages.localOnly)} />} - {mediaIcon ? ( - <Icon - fixedWidth - className='status__media-icon' - id={mediaIcon} - aria-hidden='true' - title={this.mediaIconTitleText()} - /> - ) : null} + { !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon)) } {!directMessage && <VisibilityIcon visibility={status.get('visibility')} />} {collapsible ? ( <IconButton diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index 60cc23f4b..9095e087e 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -18,7 +18,6 @@ export default class StatusList extends ImmutablePureComponent { onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, - shouldUpdateScroll: PropTypes.func, isLoading: PropTypes.bool, isPartial: PropTypes.bool, hasMore: PropTypes.bool, diff --git a/app/javascript/flavours/glitch/components/status_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index af6acdef9..5a00f232e 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.js +++ b/app/javascript/flavours/glitch/components/status_prepend.js @@ -17,7 +17,7 @@ export default class StatusPrepend extends React.PureComponent { handleClick = (e) => { const { account, parseClick } = this.props; - parseClick(e, `/accounts/${account.get('id')}`); + parseClick(e, `/@${account.get('acct')}`); } Message = () => { |