diff options
47 files changed, 541 insertions, 258 deletions
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index c9b840881..450f92bd4 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -16,30 +16,7 @@ class HomeController < ApplicationController def redirect_unauthenticated_to_permalinks! return if user_signed_in? - matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/) - - if matches - case matches[1] - when 'statuses' - status = Status.find_by(id: matches[2]) - - if status&.distributable? - redirect_to(ActivityPub::TagManager.instance.url_for(status)) - return - end - when 'accounts' - account = Account.find_by(id: matches[2]) - - if account - redirect_to(ActivityPub::TagManager.instance.url_for(account)) - return - end - end - end - - matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z}) - - redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path) + redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path) end def set_pack diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 58b636602..ce7bb6d5f 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; +export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST'; +export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS'; +export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; + export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; @@ -87,6 +91,34 @@ export function fetchAccount(id) { }; }; +export const lookupAccount = acct => (dispatch, getState) => { + dispatch(lookupAccountRequest(acct)); + + api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => { + dispatch(fetchRelationships([response.data.id])); + dispatch(importFetchedAccount(response.data)); + dispatch(lookupAccountSuccess()); + }).catch(error => { + dispatch(lookupAccountFail(acct, error)); + }); +}; + +export const lookupAccountRequest = (acct) => ({ + type: ACCOUNT_LOOKUP_REQUEST, + acct, +}); + +export const lookupAccountSuccess = () => ({ + type: ACCOUNT_LOOKUP_SUCCESS, +}); + +export const lookupAccountFail = (acct, error) => ({ + type: ACCOUNT_LOOKUP_FAIL, + acct, + error, + skipAlert: true, +}); + export function fetchAccountRequest(id) { return { type: ACCOUNT_FETCH_REQUEST, diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index c844a907f..3b3ebcb5b 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -78,7 +78,7 @@ const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1); export const ensureComposeIsVisible = (getState, routerHistory) => { if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { - routerHistory.push('/statuses/new'); + routerHistory.push('/publish'); } }; @@ -158,7 +158,7 @@ export function submitCompose(routerHistory) { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { - if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) { + if (routerHistory && routerHistory.location.pathname === '/publish' && window.history.state) { routerHistory.goBack(); } diff --git a/app/javascript/mastodon/api.js b/app/javascript/mastodon/api.js index 98d59de43..645ef6500 100644 --- a/app/javascript/mastodon/api.js +++ b/app/javascript/mastodon/api.js @@ -12,21 +12,35 @@ export const getLinks = response => { return LinkHeader.parse(value); }; -let csrfHeader = {}; +const csrfHeader = {}; -function setCSRFHeader() { +const setCSRFHeader = () => { const csrfToken = document.querySelector('meta[name=csrf-token]'); + if (csrfToken) { csrfHeader['X-CSRF-Token'] = csrfToken.content; } -} +}; ready(setCSRFHeader); +const authorizationHeaderFromState = getState => { + const accessToken = getState && getState().getIn(['meta', 'access_token'], ''); + + if (!accessToken) { + return {}; + } + + return { + 'Authorization': `Bearer ${accessToken}`, + }; +}; + export default getState => axios.create({ - headers: Object.assign(csrfHeader, getState ? { - 'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`, - } : {}), + headers: { + ...csrfHeader, + ...authorizationHeaderFromState(getState), + }, transformResponse: [function (data) { try { diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index a85d683a7..62b5843a9 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -118,7 +118,7 @@ class Account extends ImmutablePureComponent { return ( <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={`/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/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js index 75fcf20e3..1a7edf6e0 100644 --- a/app/javascript/mastodon/components/hashtag.js +++ b/app/javascript/mastodon/components/hashtag.js @@ -52,7 +52,7 @@ const Hashtag = ({ hashtag }) => ( <div className='trends__item__name'> <Permalink href={hashtag.get('url')} - to={`/timelines/tag/${hashtag.get('name')}`} + to={`/tags/${hashtag.get('name')}`} > #<span>{hashtag.get('name')}</span> </Permalink> diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index ccc9067d1..96e6a6c8d 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -134,42 +134,28 @@ class Status extends ImmutablePureComponent { this.setState({ showMedia: !this.state.showMedia }); } - handleClick = () => { - if (this.props.onClick) { - this.props.onClick(); + handleClick = e => { + if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { return; } - if (!this.context.router) { - return; + if (e) { + e.preventDefault(); } - const { status } = this.props; - this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); + this.handleHotkeyOpen(); } - handleExpandClick = (e) => { - if (this.props.onClick) { - this.props.onClick(); + handleAccountClick = e => { + if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) { return; } - if (e.button === 0) { - if (!this.context.router) { - return; - } - - const { status } = this.props; - this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); - } - } - - handleAccountClick = (e) => { - if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - const id = e.currentTarget.getAttribute('data-id'); + if (e) { e.preventDefault(); - this.context.router.history.push(`/accounts/${id}`); } + + this.handleHotkeyOpenProfile(); } handleExpandedToggle = () => { @@ -242,11 +228,30 @@ class Status extends ImmutablePureComponent { } handleHotkeyOpen = () => { - this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`); + if (this.props.onClick) { + this.props.onClick(); + return; + } + + const { router } = this.context; + const status = this._properStatus(); + + if (!router) { + return; + } + + router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); } handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`); + const { router } = this.context; + const status = this._properStatus(); + + if (!router) { + return; + } + + router.history.push(`/@${status.getIn(['account', 'acct'])}`); } handleHotkeyMoveUp = e => { @@ -465,14 +470,15 @@ class Status extends ImmutablePureComponent { {prepend} <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}> - <div className='status__expand' onClick={this.handleExpandClick} role='presentation' /> + <div className='status__expand' onClick={this.handleClick} role='presentation' /> + <div className='status__info'> - <a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'> + <a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'> <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span> <RelativeTimestamp timestamp={status.get('created_at')} /> </a> - <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> + <a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> <div className='status__avatar'> {statusAvatar} </div> diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 9981f2449..85c76edee 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -186,7 +186,7 @@ class StatusActionBar extends ImmutablePureComponent { } handleOpen = () => { - this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`); } handleEmbed = () => { diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index bf21a9fd6..d01365afb 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -112,7 +112,7 @@ export default class StatusContent extends React.PureComponent { onMentionClick = (mention, e) => { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - this.context.router.history.push(`/accounts/${mention.get('id')}`); + this.context.router.history.push(`/@${mention.get('acct')}`); } } @@ -121,7 +121,7 @@ export default class StatusContent extends React.PureComponent { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - this.context.router.history.push(`/timelines/tag/${hashtag}`); + this.context.router.history.push(`/tags/${hashtag}`); } } @@ -198,7 +198,7 @@ export default class StatusContent extends React.PureComponent { let mentionsPlaceholder = ''; const mentionLinks = status.get('mentions').map(item => ( - <Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'> + <Permalink to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='mention'> @<span>{item.get('username')}</span> </Permalink> )).reduce((aggregate, item) => [...aggregate, item, ' '], []); diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 892ff1ca9..0c3f6afa8 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -22,14 +22,38 @@ const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); store.dispatch(fetchCustomEmojis()); +const createIdentityContext = state => ({ + signedIn: !!state.meta.me, + accountId: state.meta.me, + accessToken: state.meta.access_token, +}); + export default class Mastodon extends React.PureComponent { static propTypes = { locale: PropTypes.string.isRequired, }; + static childContextTypes = { + identity: PropTypes.shape({ + signedIn: PropTypes.bool.isRequired, + accountId: PropTypes.string, + accessToken: PropTypes.string, + }).isRequired, + }; + + identity = createIdentityContext(initialState); + + getChildContext() { + return { + identity: this.identity, + }; + } + componentDidMount() { - this.disconnect = store.dispatch(connectUserStream()); + if (this.identity.signedIn) { + this.disconnect = store.dispatch(connectUserStream()); + } } componentWillUnmount () { diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 20641121f..4d0a828c7 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -332,21 +332,21 @@ class Header extends ImmutablePureComponent { {!suspended && ( <div className='account__header__extra__links'> - <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> + <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}> <ShortNumber value={account.get('statuses_count')} renderer={counterRenderer('statuses')} /> </NavLink> - <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> + <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}> <ShortNumber value={account.get('following_count')} renderer={counterRenderer('following')} /> </NavLink> - <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> + <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> <ShortNumber value={account.get('followers_count')} renderer={counterRenderer('followers')} diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index e199c929e..ef6e7c8f7 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { fetchAccount } from 'mastodon/actions/accounts'; +import { lookupAccount } from 'mastodon/actions/accounts'; import { expandAccountMediaTimeline } from '../../actions/timelines'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import Column from '../ui/components/column'; @@ -17,14 +17,25 @@ import MissingIndicator from 'mastodon/components/missing_indicator'; import { openModal } from 'mastodon/actions/modal'; import { FormattedMessage } from 'react-intl'; -const mapStateToProps = (state, props) => ({ - isAccount: !!state.getIn(['accounts', props.params.accountId]), - attachments: getAccountGallery(state, props.params.accountId), - isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), - suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false), - blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), -}); +const mapStateToProps = (state, { params: { acct } }) => { + const accountId = state.getIn(['accounts_map', acct]); + + if (!accountId) { + return { + isLoading: true, + }; + } + + return { + accountId, + isAccount: !!state.getIn(['accounts', accountId]), + attachments: getAccountGallery(state, accountId), + isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']), + hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']), + suspended: state.getIn(['accounts', accountId, 'suspended'], false), + blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), + }; +}; class LoadMoreMedia extends ImmutablePureComponent { @@ -52,7 +63,10 @@ export default @connect(mapStateToProps) class AccountGallery extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string.isRequired, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, @@ -67,15 +81,29 @@ class AccountGallery extends ImmutablePureComponent { width: 323, }; + _load () { + const { accountId, dispatch } = this.props; + + dispatch(expandAccountMediaTimeline(accountId)); + } + componentDidMount () { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); + const { params: { acct }, accountId, dispatch } = this.props; + + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); + } } - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); + componentDidUpdate (prevProps) { + const { params: { acct }, accountId, dispatch } = this.props; + + if (prevProps.accountId !== accountId && accountId) { + this._load(); + } else if (prevProps.params.acct !== acct) { + dispatch(lookupAccount(acct)); } } @@ -95,7 +123,7 @@ class AccountGallery extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); + this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId })); }; handleLoadOlder = e => { @@ -165,7 +193,7 @@ class AccountGallery extends ImmutablePureComponent { <ScrollContainer scrollKey='account_gallery'> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> - <HeaderContainer accountId={this.props.params.accountId} /> + <HeaderContainer accountId={this.props.accountId} /> {(suspended || blockedBy) ? ( <div className='empty-column-indicator'> diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 6b52defe4..17b693600 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -123,9 +123,9 @@ export default class Header extends ImmutablePureComponent { {!hideTabs && ( <div className='account__section-headline'> - <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> - <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink> - <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> + <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> + <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots and replies' /></NavLink> + <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> </div> )} </div> diff --git a/app/javascript/mastodon/features/account_timeline/components/moved_note.js b/app/javascript/mastodon/features/account_timeline/components/moved_note.js index 3e090bb5f..2e32d660f 100644 --- a/app/javascript/mastodon/features/account_timeline/components/moved_note.js +++ b/app/javascript/mastodon/features/account_timeline/components/moved_note.js @@ -21,7 +21,7 @@ export default class MovedNote extends ImmutablePureComponent { handleAccountClick = e => { if (e.button === 0) { e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.to.get('id')}`); + this.context.router.history.push(`/@${this.props.to.get('acct')}`); } e.stopPropagation(); diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 821c852b6..8ca768373 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -2,7 +2,7 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { fetchAccount } from '../../actions/accounts'; +import { lookupAccount, fetchAccount } from '../../actions/accounts'; import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; @@ -20,10 +20,19 @@ import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines' const emptyList = ImmutableList(); -const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { +const mapStateToProps = (state, { params: { acct }, withReplies = false }) => { + const accountId = state.getIn(['accounts_map', acct]); + + if (!accountId) { + return { + isLoading: true, + }; + } + const path = withReplies ? `${accountId}:with_replies` : accountId; return { + accountId, remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), @@ -48,7 +57,10 @@ export default @connect(mapStateToProps) class AccountTimeline extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string.isRequired, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list, featuredStatusIds: ImmutablePropTypes.list, @@ -63,8 +75,8 @@ class AccountTimeline extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { - const { params: { accountId }, withReplies, dispatch } = this.props; + _load () { + const { accountId, withReplies, dispatch } = this.props; dispatch(fetchAccount(accountId)); dispatch(fetchAccountIdentityProofs(accountId)); @@ -80,29 +92,32 @@ class AccountTimeline extends ImmutablePureComponent { } } - componentWillReceiveProps (nextProps) { - const { dispatch } = this.props; + componentDidMount () { + const { params: { acct }, accountId, dispatch } = this.props; - if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { - dispatch(fetchAccount(nextProps.params.accountId)); - dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); + } + } - if (!nextProps.withReplies) { - dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); - } + componentDidUpdate (prevProps) { + const { params: { acct }, accountId, dispatch } = this.props; - dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); + if (prevProps.accountId !== accountId && accountId) { + this._load(); + } else if (prevProps.params.acct !== acct) { + dispatch(lookupAccount(acct)); } - if (nextProps.params.accountId === me && this.props.params.accountId !== me) { - dispatch(connectTimeline(`account:${me}`)); - } else if (this.props.params.accountId === me && nextProps.params.accountId !== me) { + if (prevProps.accountId === me && accountId !== me) { dispatch(disconnectTimeline(`account:${me}`)); } } componentWillUnmount () { - const { dispatch, params: { accountId } } = this.props; + const { dispatch, accountId } = this.props; if (accountId === me) { dispatch(disconnectTimeline(`account:${me}`)); @@ -110,7 +125,7 @@ class AccountTimeline extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); + this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies })); } render () { @@ -152,7 +167,7 @@ class AccountTimeline extends ImmutablePureComponent { <ColumnBackButton multiColumn={multiColumn} /> <StatusList - prepend={<HeaderContainer accountId={this.props.params.accountId} />} + prepend={<HeaderContainer accountId={this.props.accountId} />} alwaysPrepend append={remoteMessage} scrollKey='account_timeline' diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js index 840d0a3da..e6ba7d8b7 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.js +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js @@ -19,13 +19,13 @@ export default class NavigationBar extends ImmutablePureComponent { render () { return ( <div className='navigation-bar'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}> <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <Avatar account={this.props.account} size={48} /> </Permalink> <div className='navigation-bar__profile'> - <Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <Permalink href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}> <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> </Permalink> diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js index a1d5c420c..863defb76 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.js +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js @@ -32,7 +32,7 @@ class ReplyIndicator extends ImmutablePureComponent { handleAccountClick = (e) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); } } diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index c1bce0a3f..3d2796cd3 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -100,16 +100,16 @@ class Compose extends React.PureComponent { <nav className='drawer__header'> <Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link> {!columns.some(column => column.get('id') === 'HOME') && ( - <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link> + <Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link> )} {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link> )} {!columns.some(column => column.get('id') === 'COMMUNITY') && ( - <Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> + <Link to='/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link> )} {!columns.some(column => column.get('id') === 'PUBLIC') && ( - <Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> + <Link to='/federated' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link> )} <a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a> <a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' fixedWidth /></a> diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js index 43e1d77b9..c4f7098c5 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -133,7 +133,7 @@ class Conversation extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); - const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]); + const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]); const handlers = { reply: this.handleReply, diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js index 8f0e8db4b..03e13f28e 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.js +++ b/app/javascript/mastodon/features/directory/components/account_card.js @@ -213,7 +213,7 @@ class AccountCard extends ImmutablePureComponent { <Permalink className='directory__card__bar__name' href={account.get('url')} - to={`/accounts/${account.get('id')}`} + to={`/@${account.get('acct')}`} > <Avatar account={account} size={48} /> <DisplayName account={account} /> diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js index 8269f5ae4..263a7ae16 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js @@ -30,7 +30,7 @@ class AccountAuthorize extends ImmutablePureComponent { return ( <div className='account-authorize__wrapper'> <div className='account-authorize'> - <Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name'> + <Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'> <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div> <DisplayName account={account} /> </Permalink> diff --git a/app/javascript/mastodon/features/followers/index.js b/app/javascript/mastodon/features/followers/index.js index ee747f0da..3193ce322 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; import LoadingIndicator from '../../components/loading_indicator'; import { - fetchAccount, + lookupAccount, fetchFollowers, expandFollowers, } from '../../actions/accounts'; @@ -19,15 +19,26 @@ import ScrollableList from '../../components/scrollable_list'; import MissingIndicator from 'mastodon/components/missing_indicator'; import TimelineHint from 'mastodon/components/timeline_hint'; -const mapStateToProps = (state, props) => ({ - remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])), - remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']), - isAccount: !!state.getIn(['accounts', props.params.accountId]), - accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), - hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), - isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true), - blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), -}); +const mapStateToProps = (state, { params: { acct } }) => { + const accountId = state.getIn(['accounts_map', acct]); + + if (!accountId) { + return { + isLoading: true, + }; + } + + return { + accountId, + remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), + remoteUrl: state.getIn(['accounts', accountId, 'url']), + isAccount: !!state.getIn(['accounts', accountId]), + accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']), + hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']), + isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true), + blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), + }; +}; const RemoteHint = ({ url }) => ( <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} /> @@ -41,7 +52,10 @@ export default @connect(mapStateToProps) class Followers extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string.isRequired, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, @@ -53,22 +67,34 @@ class Followers extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { - if (!this.props.accountIds) { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowers(this.props.params.accountId)); + _load () { + const { accountId, dispatch } = this.props; + + dispatch(fetchFollowers(accountId)); + } + + componentDidMount () { + const { params: { acct }, accountId, dispatch } = this.props; + + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); } } - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(fetchFollowers(nextProps.params.accountId)); + componentDidUpdate (prevProps) { + const { params: { acct }, accountId, dispatch } = this.props; + + if (prevProps.accountId !== accountId && accountId) { + this._load(); + } else if (prevProps.params.acct !== acct) { + dispatch(lookupAccount(acct)); } } handleLoadMore = debounce(() => { - this.props.dispatch(expandFollowers(this.props.params.accountId)); + this.props.dispatch(expandFollowers(this.props.accountId)); }, 300, { leading: true }); render () { @@ -111,7 +137,7 @@ class Followers extends ImmutablePureComponent { hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} - prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} + prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} alwaysPrepend append={remoteMessage} emptyMessage={emptyMessage} diff --git a/app/javascript/mastodon/features/following/index.js b/app/javascript/mastodon/features/following/index.js index 804df803e..d26ada277 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; import LoadingIndicator from '../../components/loading_indicator'; import { - fetchAccount, + lookupAccount, fetchFollowing, expandFollowing, } from '../../actions/accounts'; @@ -19,15 +19,26 @@ import ScrollableList from '../../components/scrollable_list'; import MissingIndicator from 'mastodon/components/missing_indicator'; import TimelineHint from 'mastodon/components/timeline_hint'; -const mapStateToProps = (state, props) => ({ - remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])), - remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']), - isAccount: !!state.getIn(['accounts', props.params.accountId]), - accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), - hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), - isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true), - blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), -}); +const mapStateToProps = (state, { params: { acct } }) => { + const accountId = state.getIn(['accounts_map', acct]); + + if (!accountId) { + return { + isLoading: true, + }; + } + + return { + accountId, + remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), + remoteUrl: state.getIn(['accounts', accountId, 'url']), + isAccount: !!state.getIn(['accounts', accountId]), + accountIds: state.getIn(['user_lists', 'following', accountId, 'items']), + hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']), + isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true), + blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), + }; +}; const RemoteHint = ({ url }) => ( <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} /> @@ -41,7 +52,10 @@ export default @connect(mapStateToProps) class Following extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string.isRequired, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, @@ -53,22 +67,34 @@ class Following extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { - if (!this.props.accountIds) { - this.props.dispatch(fetchAccount(this.props.params.accountId)); - this.props.dispatch(fetchFollowing(this.props.params.accountId)); + _load () { + const { accountId, dispatch } = this.props; + + dispatch(fetchFollowing(accountId)); + } + + componentDidMount () { + const { params: { acct }, accountId, dispatch } = this.props; + + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); } } - componentWillReceiveProps (nextProps) { - if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(fetchFollowing(nextProps.params.accountId)); + componentDidUpdate (prevProps) { + const { params: { acct }, accountId, dispatch } = this.props; + + if (prevProps.accountId !== accountId && accountId) { + this._load(); + } else if (prevProps.params.acct !== acct) { + dispatch(lookupAccount(acct)); } } handleLoadMore = debounce(() => { - this.props.dispatch(expandFollowing(this.props.params.accountId)); + this.props.dispatch(expandFollowing(this.props.accountId)); }, 300, { leading: true }); render () { @@ -111,7 +137,7 @@ class Following extends ImmutablePureComponent { hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} - prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} + prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />} alwaysPrepend append={remoteMessage} emptyMessage={emptyMessage} diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index ff1566e05..24db8cede 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -87,7 +87,7 @@ class Content extends ImmutablePureComponent { onMentionClick = (mention, e) => { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - this.context.router.history.push(`/accounts/${mention.get('id')}`); + this.context.router.history.push(`/@${mention.get('acct')}`); } } @@ -96,14 +96,14 @@ class Content extends ImmutablePureComponent { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - this.context.router.history.push(`/timelines/tag/${hashtag}`); + this.context.router.history.push(`/tags/${hashtag}`); } } onStatusClick = (status, e) => { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); - this.context.router.history.push(`/statuses/${status.get('id')}`); + this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); } } diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 1b9994612..5508adb80 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -82,7 +82,7 @@ class GettingStarted extends ImmutablePureComponent { const { fetchFollowRequests, multiColumn } = this.props; if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { - this.context.router.history.replace('/timelines/home'); + this.context.router.history.replace('/home'); return; } @@ -98,8 +98,8 @@ class GettingStarted extends ImmutablePureComponent { if (multiColumn) { navItems.push( <ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />, - <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />, - <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />, + <ColumnLink key='community_timeline' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />, + <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />, ); height += 34 + 48*2; @@ -127,13 +127,13 @@ class GettingStarted extends ImmutablePureComponent { if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) { navItems.push( - <ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />, + <ColumnLink key='home' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />, ); height += 48; } navItems.push( - <ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />, + <ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />, <ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />, <ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />, diff --git a/app/javascript/mastodon/features/lists/index.js b/app/javascript/mastodon/features/lists/index.js index 29c311809..809d79d99 100644 --- a/app/javascript/mastodon/features/lists/index.js +++ b/app/javascript/mastodon/features/lists/index.js @@ -73,7 +73,7 @@ class Lists extends ImmutablePureComponent { bindToDocument={!multiColumn} > {lists.map(list => - <ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />, + <ColumnLink key={list.get('id')} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} />, )} </ScrollableList> </Column> diff --git a/app/javascript/mastodon/features/notifications/components/follow_request.js b/app/javascript/mastodon/features/notifications/components/follow_request.js index a80cfb2fa..9ef3fde7e 100644 --- a/app/javascript/mastodon/features/notifications/components/follow_request.js +++ b/app/javascript/mastodon/features/notifications/components/follow_request.js @@ -42,7 +42,7 @@ class FollowRequest extends ImmutablePureComponent { return ( <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={`/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> <DisplayName account={account} /> </Permalink> diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 94fdbd6f4..f9f8a87f2 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -68,7 +68,7 @@ class Notification extends ImmutablePureComponent { const { notification } = this.props; if (notification.get('status')) { - this.context.router.history.push(`/statuses/${notification.get('status')}`); + this.context.router.history.push(`/@${notification.getIn(['status', 'account', 'acct'])}/${notification.get('status')}`); } else { this.handleOpenProfile(); } @@ -76,7 +76,7 @@ class Notification extends ImmutablePureComponent { handleOpenProfile = () => { const { notification } = this.props; - this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); + this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`); } handleMention = e => { @@ -315,7 +315,7 @@ class Notification extends ImmutablePureComponent { const { notification } = this.props; const account = notification.get('account'); const displayNameHtml = { __html: account.get('display_name_html') }; - const link = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>; + const link = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>; switch(notification.get('type')) { case 'follow': diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.js b/app/javascript/mastodon/features/picture_in_picture/components/footer.js index f5ce50ac8..0de562ee1 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.js @@ -120,7 +120,7 @@ class Footer extends ImmutablePureComponent { onClose(); } - router.history.push(`/statuses/${status.get('id')}`); + router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); } render () { diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.js b/app/javascript/mastodon/features/picture_in_picture/components/header.js index 7dd199b75..e05d8c62e 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/header.js +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.js @@ -34,7 +34,7 @@ class Header extends ImmutablePureComponent { return ( <div className='picture-in-picture__header'> - <Link to={`/statuses/${statusId}`} className='picture-in-picture__header__account'> + <Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'> <Avatar account={account} size={36} /> <DisplayName account={account} /> </Link> diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 043a749ed..72ddeb2b2 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -55,7 +55,7 @@ class DetailedStatus extends ImmutablePureComponent { handleAccountClick = (e) => { if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { e.preventDefault(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); } e.stopPropagation(); @@ -195,7 +195,7 @@ class DetailedStatus extends ImmutablePureComponent { reblogLink = ( <React.Fragment> <React.Fragment> · </React.Fragment> - <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> + <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'> <Icon id={reblogIcon} /> <span className='detailed-status__reblogs'> <AnimatedNumber value={status.get('reblogs_count')} /> @@ -219,7 +219,7 @@ class DetailedStatus extends ImmutablePureComponent { if (this.context.router) { favouriteLink = ( - <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> + <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'> <Icon id='star' /> <span className='detailed-status__favorites'> <AnimatedNumber value={status.get('favourites_count')} /> diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 69cd245fb..055963d14 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -396,7 +396,7 @@ class Status extends ImmutablePureComponent { } handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); } handleHotkeyToggleHidden = () => { diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.js b/app/javascript/mastodon/features/ui/components/boost_modal.js index 83229833b..f8a344690 100644 --- a/app/javascript/mastodon/features/ui/components/boost_modal.js +++ b/app/javascript/mastodon/features/ui/components/boost_modal.js @@ -68,7 +68,7 @@ class BoostModal extends ImmutablePureComponent { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); this.props.onClose(); - this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); } } diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 039abe432..651675c19 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -216,7 +216,7 @@ class ColumnsArea extends ImmutablePureComponent { const columnIndex = getIndex(this.context.router.history.location.pathname); if (singleColumn) { - const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; + const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/publish' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>; const content = columnIndex !== -1 ? ( <ReactSwipeableViews key='content' hysteresis={0.2} threshold={15} index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }} disabled={disableSwiping}> diff --git a/app/javascript/mastodon/features/ui/components/list_panel.js b/app/javascript/mastodon/features/ui/components/list_panel.js index 1f7ec683a..411f62508 100644 --- a/app/javascript/mastodon/features/ui/components/list_panel.js +++ b/app/javascript/mastodon/features/ui/components/list_panel.js @@ -46,7 +46,7 @@ class ListPanel extends ImmutablePureComponent { <hr /> {lists.map(list => ( - <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/timelines/list/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink> + <NavLink key={list.get('id')} className='column-link column-link--transparent' strict to={`/lists/${list.get('id')}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.get('title')}</NavLink> ))} </div> ); diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 061776e24..ae937d1cd 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -128,13 +128,6 @@ class MediaModal extends ImmutablePureComponent { })); }; - handleStatusClick = e => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.context.router.history.push(`/statuses/${this.props.statusId}`); - } - } - render () { const { media, statusId, intl, onClose } = this.props; const { navigationHidden } = this.state; diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js index 0c12852f5..901dbdfcb 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_panel.js +++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js @@ -10,12 +10,12 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends const NavigationPanel = () => ( <div className='navigation-panel'> - <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> <FollowRequestsNavLink /> - <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> - <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> - <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> + <NavLink className='column-link column-link--transparent' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> + <NavLink className='column-link column-link--transparent' to='/conversations'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink> <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index 1911da8ba..a023bcf34 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -8,10 +8,10 @@ import Icon from 'mastodon/components/icon'; import NotificationsCounterIcon from './notifications_counter_icon'; export const links = [ - <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, + <NavLink className='tabs-bar__link' to='/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, - <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, - <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, + <NavLink className='tabs-bar__link' to='/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, + <NavLink className='tabs-bar__link' exact to='/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, ]; diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 756a69293..af8d62826 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -72,6 +72,7 @@ const mapStateToProps = state => ({ canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4, dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, + username: state.getIn(['accounts', me, 'username']), }); const keyMap = { @@ -144,7 +145,7 @@ class SwitchingColumnsArea extends React.PureComponent { render () { const { children, mobile } = this.props; - const redirect = mobile ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; + const redirect = mobile ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />; return ( <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> @@ -152,32 +153,32 @@ class SwitchingColumnsArea extends React.PureComponent { {redirect} <WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> - <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> - <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> - <WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} /> - <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} /> - <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> - <WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} /> + <WrappedRoute path='/home' component={HomeTimeline} content={children} /> + <WrappedRoute path='/public' exact component={PublicTimeline} content={children} /> + <WrappedRoute path='/public/local' exact component={CommunityTimeline} content={children} /> + <WrappedRoute path='/conversations' component={DirectTimeline} content={children} /> + <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} /> + <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> + <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> <WrappedRoute path='/start' component={FollowRecommendations} content={children} /> <WrappedRoute path='/search' component={Search} content={children} /> <WrappedRoute path='/directory' component={Directory} content={children} /> + <WrappedRoute path='/publish' component={Compose} content={children} /> - <WrappedRoute path='/statuses/new' component={Compose} content={children} /> - <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> - <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> - <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> - - <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> - <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> - <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> - <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> - <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> + <WrappedRoute path='/@:acct' exact component={AccountTimeline} content={children} /> + <WrappedRoute path='/@:acct/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> + <WrappedRoute path='/@:acct/followers' component={Followers} content={children} /> + <WrappedRoute path='/@:acct/following' component={Following} content={children} /> + <WrappedRoute path='/@:acct/media' component={AccountGallery} content={children} /> + <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} /> + <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} /> + <WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} /> <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} /> @@ -214,6 +215,7 @@ class UI extends React.PureComponent { dropdownMenuIsOpen: PropTypes.bool, layout: PropTypes.string.isRequired, firstLaunch: PropTypes.bool, + username: PropTypes.string, }; state = { @@ -451,7 +453,7 @@ class UI extends React.PureComponent { } handleHotkeyGoToHome = () => { - this.context.router.history.push('/timelines/home'); + this.context.router.history.push('/home'); } handleHotkeyGoToNotifications = () => { @@ -459,15 +461,15 @@ class UI extends React.PureComponent { } handleHotkeyGoToLocal = () => { - this.context.router.history.push('/timelines/public/local'); + this.context.router.history.push('/public/local'); } handleHotkeyGoToFederated = () => { - this.context.router.history.push('/timelines/public'); + this.context.router.history.push('/public'); } handleHotkeyGoToDirect = () => { - this.context.router.history.push('/timelines/direct'); + this.context.router.history.push('/conversations'); } handleHotkeyGoToStart = () => { @@ -483,7 +485,7 @@ class UI extends React.PureComponent { } handleHotkeyGoToProfile = () => { - this.context.router.history.push(`/accounts/${me}`); + this.context.router.history.push(`/@${this.props.username}`); } handleHotkeyGoToBlocked = () => { diff --git a/app/javascript/mastodon/reducers/accounts_map.js b/app/javascript/mastodon/reducers/accounts_map.js new file mode 100644 index 000000000..e0d42e9cd --- /dev/null +++ b/app/javascript/mastodon/reducers/accounts_map.js @@ -0,0 +1,15 @@ +import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; +import { Map as ImmutableMap } from 'immutable'; + +const initialState = ImmutableMap(); + +export default function accountsMap(state = initialState, action) { + switch(action.type) { + case ACCOUNT_IMPORT: + return state.set(action.account.acct, action.account.id); + case ACCOUNTS_IMPORT: + return state.withMutations(map => action.accounts.forEach(account => map.set(account.acct, account.id))); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3b3c5ae29..e518c8228 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -38,6 +38,7 @@ import missed_updates from './missed_updates'; import announcements from './announcements'; import markers from './markers'; import picture_in_picture from './picture_in_picture'; +import accounts_map from './accounts_map'; const reducers = { announcements, @@ -52,6 +53,7 @@ const reducers = { status_lists, accounts, accounts_counters, + accounts_map, statuses, relationships, settings, diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 958e5fc12..926c5c4d7 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -90,7 +90,7 @@ const handlePush = (event) => { options.tag = notification.id; options.badge = '/badge.png'; options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; - options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` }; + options.data = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/@${notification.account.acct}/${notification.status.id}` : `/web/@${notification.account.acct}` }; if (notification.status && notification.status.spoiler_text || notification.status.sensitive) { options.data.hiddenBody = htmlToPlainText(notification.status.content); diff --git a/app/lib/permalink_redirector.rb b/app/lib/permalink_redirector.rb new file mode 100644 index 000000000..e48bce060 --- /dev/null +++ b/app/lib/permalink_redirector.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class PermalinkRedirector + include RoutingHelper + + def initialize(path) + @path = path + end + + def redirect_path + if path_segments[0] == 'web' + if path_segments[1].present? && path_segments[1].start_with?('@') && path_segments[2] =~ /\d/ + find_status_url_by_id(path_segments[2]) + elsif path_segments[1].present? && path_segments[1].start_with?('@') + find_account_url_by_name(path_segments[1]) + elsif path_segments[1] == 'statuses' && path_segments[2] =~ /\d/ + find_status_url_by_id(path_segments[2]) + elsif path_segments[1] == 'accounts' && path_segments[2] =~ /\d/ + find_account_url_by_id(path_segments[2]) + elsif path_segments[1] == 'timelines' && path_segments[2] == 'tag' && path_segments[3].present? + find_tag_url_by_name(path_segments[3]) + elsif path_segments[1] == 'tags' && path_segments[2].present? + find_tag_url_by_name(path_segments[2]) + end + end + end + + private + + def path_segments + @path_segments ||= @path.gsub(/\A\//, '').split('/') + end + + def find_status_url_by_id(id) + status = Status.find_by(id: id) + + return unless status&.distributable? + + ActivityPub::TagManager.instance.url_for(status) + end + + def find_account_url_by_id(id) + account = Account.find_by(id: id) + + return unless account + + ActivityPub::TagManager.instance.url_for(account) + end + + def find_account_url_by_name(name) + username, domain = name.gsub(/\A@/, '').split('@') + domain = nil if TagManager.instance.local_domain?(domain) + account = Account.find_remote(username, domain) + + return unless account + + ActivityPub::TagManager.instance.url_for(account) + end + + def find_tag_url_by_name(name) + tag_path(CGI.unescape(name)) + end +end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index fc187db40..a1b5ca1e3 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -127,7 +127,7 @@ class NotifyService < BaseService def push_notification! return if @notification.activity.nil? - Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) + Redis.current.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) send_push_notifications! end diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index 941f1dd91..70c5c42c5 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -8,8 +8,10 @@ RSpec.describe HomeController, type: :controller do context 'when not signed in' do context 'when requested path is tag timeline' do - before { @request.path = '/web/timelines/tag/name' } - it { is_expected.to redirect_to '/tags/name' } + it 'redirects to the tag\'s permalink' do + @request.path = '/web/timelines/tag/name' + is_expected.to redirect_to '/tags/name' + end end it 'redirects to about page' do diff --git a/spec/lib/permalink_redirector_spec.rb b/spec/lib/permalink_redirector_spec.rb new file mode 100644 index 000000000..b916b33b2 --- /dev/null +++ b/spec/lib/permalink_redirector_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PermalinkRedirector do + describe '#redirect_url' do + before do + account = Fabricate(:account, username: 'alice', id: 1) + Fabricate(:status, account: account, id: 123) + end + + it 'returns path for legacy account links' do + redirector = described_class.new('web/accounts/1') + expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice' + end + + it 'returns path for legacy status links' do + redirector = described_class.new('web/statuses/123') + expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123' + end + + it 'returns path for legacy tag links' do + redirector = described_class.new('web/timelines/tag/hoge') + expect(redirector.redirect_path).to eq '/tags/hoge' + end + + it 'returns path for pretty account links' do + redirector = described_class.new('web/@alice') + expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice' + end + + it 'returns path for pretty status links' do + redirector = described_class.new('web/@alice/123') + expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123' + end + + it 'returns path for pretty tag links' do + redirector = described_class.new('web/tags/hoge') + expect(redirector.redirect_path).to eq '/tags/hoge' + end + end +end diff --git a/streaming/index.js b/streaming/index.js index ac8e80fb9..9f8ab107e 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -283,6 +283,14 @@ const startWorker = (workerId) => { }; /** + * @param {any} req + * @param {string[]} necessaryScopes + * @return {boolean} + */ + const isInScope = (req, necessaryScopes) => + req.scopes.some(scope => necessaryScopes.includes(scope)); + + /** * @param {string} token * @param {any} req * @return {Promise.<void>} @@ -314,7 +322,6 @@ const startWorker = (workerId) => { req.scopes = result.rows[0].scopes.split(' '); req.accountId = result.rows[0].account_id; req.chosenLanguages = result.rows[0].chosen_languages; - req.allowNotifications = req.scopes.some(scope => ['read', 'read:notifications'].includes(scope)); req.deviceId = result.rows[0].device_id; resolve(); @@ -581,15 +588,13 @@ const startWorker = (workerId) => { * @param {function(string, string): void} output * @param {function(string[], function(string): void): void} attachCloseHandler * @param {boolean=} needsFiltering - * @param {boolean=} notificationOnly * @param {boolean=} allowLocalOnly * @return {function(string): void} */ - const streamFrom = (ids, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false, allowLocalOnly = false) => { + const streamFrom = (ids, req, output, attachCloseHandler, needsFiltering = false, allowLocalOnly = false) => { const accountId = req.accountId || req.remoteAddress; - const streamType = notificationOnly ? ' (notification)' : ''; - log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}${streamType}`); + log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`); const listener = message => { const json = parseJSON(message); @@ -607,14 +612,6 @@ const startWorker = (workerId) => { output(event, encodedPayload); }; - if (notificationOnly && event !== 'notification') { - return; - } - - if (event === 'notification' && !req.allowNotifications) { - return; - } - // Only send local-only statuses to logged-in users if (event === 'update' && payload.local_only && !(req.accountId && allowLocalOnly)) { log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`); @@ -767,7 +764,7 @@ const startWorker = (workerId) => { const onSend = streamToHttp(req, res); const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds)); - streamFrom(channelIds, req, onSend, onEnd, options.needsFiltering, options.notificationOnly, options.allowLocalOnly); + streamFrom(channelIds, req, onSend, onEnd, options.needsFiltering, options.allowLocalOnly); }).catch(err => { log.verbose(req.requestId, 'Subscription error:', err.toString()); httpNotFound(res); @@ -785,86 +782,104 @@ const startWorker = (workerId) => { /** * @param {any} req + * @return {string[]} + */ + const channelsForUserStream = req => { + const arr = [`timeline:${req.accountId}`]; + + if (isInScope(req, ['crypto']) && req.deviceId) { + arr.push(`timeline:${req.accountId}:${req.deviceId}`); + } + + if (isInScope(req, ['read', 'read:notifications'])) { + arr.push(`timeline:${req.accountId}:notifications`); + } + + return arr; + }; + + /** + * @param {any} req * @param {string} name * @param {StreamParams} params - * @return {Promise.<{ channelIds: string[], options: { needsFiltering: boolean, notificationOnly: boolean } }>} + * @return {Promise.<{ channelIds: string[], options: { needsFiltering: boolean } }>} */ const channelNameToIds = (req, name, params) => new Promise((resolve, reject) => { switch(name) { case 'user': resolve({ - channelIds: req.deviceId ? [`timeline:${req.accountId}`, `timeline:${req.accountId}:${req.deviceId}`] : [`timeline:${req.accountId}`], - options: { needsFiltering: false, notificationOnly: false, allowLocalOnly: true }, + channelIds: channelsForUserStream(req), + options: { needsFiltering: false, allowLocalOnly: true }, }); break; case 'user:notification': resolve({ - channelIds: [`timeline:${req.accountId}`], - options: { needsFiltering: false, notificationOnly: true, allowLocalOnly: true }, + channelIds: [`timeline:${req.accountId}:notifications`], + options: { needsFiltering: false, allowLocalOnly: true }, }); break; case 'public': resolve({ channelIds: ['timeline:public'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: isTruthy(params.allow_local_only) }, + options: { needsFiltering: true, allowLocalOnly: isTruthy(params.allow_local_only) }, }); break; case 'public:allow_local_only': resolve({ channelIds: ['timeline:public'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: true, allowLocalOnly: true }, }); break; case 'public:local': resolve({ channelIds: ['timeline:public:local'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: true, allowLocalOnly: true }, }); break; case 'public:remote': resolve({ channelIds: ['timeline:public:remote'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: false }, + options: { needsFiltering: true, allowLocalOnly: false }, }); break; case 'public:media': resolve({ channelIds: ['timeline:public:media'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: isTruthy(query.allow_local_only) }, + options: { needsFiltering: true, allowLocalOnly: isTruthy(query.allow_local_only) }, }); break; case 'public:allow_local_only:media': resolve({ channelIds: ['timeline:public:media'], - options: { needsFiltering: true, notificationsOnly: false, allowLocalOnly: true }, + options: { needsFiltering: true, allowLocalOnly: true }, }); break; case 'public:local:media': resolve({ channelIds: ['timeline:public:local:media'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: true, allowLocalOnly: true }, }); break; case 'public:remote:media': resolve({ channelIds: ['timeline:public:remote:media'], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: false }, + options: { needsFiltering: true, allowLocalOnly: false }, }); break; case 'direct': resolve({ channelIds: [`timeline:direct:${req.accountId}`], - options: { needsFiltering: false, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: false, allowLocalOnly: true }, }); break; @@ -874,7 +889,7 @@ const startWorker = (workerId) => { } else { resolve({ channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}`], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: true, allowLocalOnly: true }, }); } @@ -885,7 +900,7 @@ const startWorker = (workerId) => { } else { resolve({ channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}:local`], - options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: true, allowLocalOnly: true }, }); } @@ -894,7 +909,7 @@ const startWorker = (workerId) => { authorizeListAccess(params.list, req).then(() => { resolve({ channelIds: [`timeline:list:${params.list}`], - options: { needsFiltering: false, notificationOnly: false, allowLocalOnly: true }, + options: { needsFiltering: false, allowLocalOnly: true }, }); }).catch(() => { reject('Not authorized to stream this list'); @@ -941,7 +956,8 @@ const startWorker = (workerId) => { const onSend = streamToWs(request, socket, streamNameFromChannelName(channelName, params)); const stopHeartbeat = subscriptionHeartbeat(channelIds); - const listener = streamFrom(channelIds, request, onSend, undefined, options.needsFiltering, options.notificationOnly, options.allowLocalOnly); + + const listener = streamFrom(channelIds, request, onSend, undefined, options.needsFiltering, options.allowLocalOnly); subscriptions[channelIds.join(';')] = { listener, |