diff options
author | Claire <claire.github-309c@sitedethib.com> | 2021-09-28 14:10:30 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-28 14:10:30 +0200 |
commit | 00889b313176308018d66e54b5cb7dcc92da1587 (patch) | |
tree | 2123cb37613d91a9446d9030e6d43600eea6a7e8 /app | |
parent | 4b7e43602691193b5d2a8e7e0ed6044bc8ee9774 (diff) | |
parent | c4ccbbccabf2ed6a899e3ca46fa9c2f8bf72b269 (diff) |
Merge pull request #1609 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
Diffstat (limited to 'app')
93 files changed, 862 insertions, 420 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/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 912a3d179..0cf64e076 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/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'; @@ -104,6 +108,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/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index eebe98626..96931546c 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -83,7 +83,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'); } }; @@ -176,7 +176,8 @@ export function submitCompose(routerHistory) { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, }).then(function (response) { - if (routerHistory && routerHistory.location.pathname === '/statuses/new' + if (routerHistory + && (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new') && window.history.state && !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) { routerHistory.goBack(); 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/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/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/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js index 24c595ed7..d00c01e77 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index b72c2417c..72d59f55a 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,13 @@ 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); + 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 => { diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 206ae74c8..d63c6b142 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -148,10 +148,10 @@ class StatusActionBar extends ImmutablePureComponent { handleOpen = () => { let state = {...this.context.router.history.location.state}; if (state.mastodonModalKey) { - this.context.router.history.replace(`/statuses/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 }); + 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); } } diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 82d066432..1d32b35e5 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -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}`); } } @@ -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' 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_prepend.js b/app/javascript/flavours/glitch/components/status_prepend.js index af6acdef9..5f8d70c9a 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 = () => { diff --git a/app/javascript/flavours/glitch/containers/mastodon.js b/app/javascript/flavours/glitch/containers/mastodon.js index 131303fd3..de8ea8ee2 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.js +++ b/app/javascript/flavours/glitch/containers/mastodon.js @@ -23,14 +23,38 @@ store.dispatch(hydrateAction); // load custom emojis 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/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index 2d4cc7f49..23120d57e 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -62,17 +62,17 @@ class ActionBar extends React.PureComponent { <div className='account__action-bar'> <div className='account__action-bar-links'> - <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> + <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}`}> <FormattedMessage id='account.posts' defaultMessage='Posts' /> <strong><FormattedNumber value={account.get('statuses_count')} /></strong> </NavLink> - <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> + <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/following`}> <FormattedMessage id='account.follows' defaultMessage='Follows' /> <strong><FormattedNumber value={account.get('following_count')} /></strong> </NavLink> - <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> + <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/@${account.get('acct')}/followers`}> <FormattedMessage id='account.followers' defaultMessage='Followers' /> <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong> </NavLink> diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 434a47dfc..8df1bf4ca 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/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 'flavours/glitch/actions/accounts'; +import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import Column from 'flavours/glitch/features/ui/components/column'; @@ -16,13 +16,24 @@ import LoadMore from 'flavours/glitch/components/load_more'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import { openModal } from 'flavours/glitch/actions/modal'; -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), -}); +const mapStateToProps = (state, { params: { acct, id } }) => { + const accountId = id || 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), + }; +}; class LoadMoreMedia extends ImmutablePureComponent { @@ -50,7 +61,11 @@ export default @connect(mapStateToProps) class AccountGallery extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, @@ -64,15 +79,30 @@ class AccountGallery extends ImmutablePureComponent { width: 323, }; + _load () { + const { accountId, isAccount, dispatch } = this.props; + + if (!isAccount) dispatch(fetchAccount(accountId)); + 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)); } } @@ -96,7 +126,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 => { @@ -162,7 +192,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 ? ( <div className='empty-column-indicator'> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index a6b57d331..e70f011b7 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -128,9 +128,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 with 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 with replies' /></NavLink> + <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> </div> )} </div> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js index fcaa7b494..308407e94 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js @@ -23,7 +23,7 @@ export default class MovedNote extends ImmutablePureComponent { e.preventDefault(); let state = {...this.context.router.history.location.state}; state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/accounts/${this.props.to.get('id')}`, state); + this.context.router.history.push(`/@${this.props.to.get('acct')}`, state); } e.stopPropagation(); diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 0d24980a9..776687486 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/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 'flavours/glitch/actions/accounts'; +import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; @@ -19,10 +19,19 @@ import TimelineHint from 'flavours/glitch/components/timeline_hint'; const emptyList = ImmutableList(); -const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { +const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => { + const accountId = id || 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]), @@ -46,7 +55,11 @@ export default @connect(mapStateToProps) class AccountTimeline extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list, featuredStatusIds: ImmutablePropTypes.list, @@ -60,25 +73,47 @@ class AccountTimeline extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { - const { params: { accountId }, withReplies } = this.props; + _load () { + const { accountId, withReplies, dispatch } = this.props; - this.props.dispatch(fetchAccount(accountId)); - this.props.dispatch(fetchAccountIdentityProofs(accountId)); + dispatch(fetchAccount(accountId)); + dispatch(fetchAccountIdentityProofs(accountId)); if (!withReplies) { - this.props.dispatch(expandAccountFeaturedTimeline(accountId)); + dispatch(expandAccountFeaturedTimeline(accountId)); + } + dispatch(expandAccountTimeline(accountId, { withReplies })); + } + + componentDidMount () { + const { params: { acct }, accountId, dispatch } = this.props; + + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); + } + } + + 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)); } - this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); } componentWillReceiveProps (nextProps) { + const { dispatch } = this.props; + if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { - this.props.dispatch(fetchAccount(nextProps.params.accountId)); - this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); + dispatch(fetchAccount(nextProps.params.accountId)); + dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); if (!nextProps.withReplies) { - this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); + dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } - this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); + dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); } } @@ -87,7 +122,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 })); } setRef = c => { @@ -131,7 +166,7 @@ class AccountTimeline extends ImmutablePureComponent { <ProfileColumnHeader onClick={this.handleHeaderClick} 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/flavours/glitch/features/compose/components/header.js b/app/javascript/flavours/glitch/features/compose/components/header.js index 5b456b717..c6e4cc36b 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.js +++ b/app/javascript/flavours/glitch/features/compose/components/header.js @@ -87,7 +87,7 @@ class Header extends ImmutablePureComponent { <Link aria-label={intl.formatMessage(messages.home_timeline)} title={intl.formatMessage(messages.home_timeline)} - to='/timelines/home' + to='/home' ><Icon id='home' /></Link> ))} {renderForColumn('NOTIFICATIONS', ( @@ -106,14 +106,14 @@ class Header extends ImmutablePureComponent { <Link aria-label={intl.formatMessage(messages.community)} title={intl.formatMessage(messages.community)} - to='/timelines/public/local' + to='/public/local' ><Icon id='users' /></Link> ))} {renderForColumn('PUBLIC', ( <Link aria-label={intl.formatMessage(messages.public)} title={intl.formatMessage(messages.public)} - to='/timelines/public' + to='/public' ><Icon id='globe' /></Link> ))} <a diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js index f6bfbdd1e..595ca5512 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js @@ -15,13 +15,13 @@ export default class NavigationBar extends ImmutablePureComponent { render () { return ( <div className='drawer--account'> - <Permalink className='avatar' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <Permalink className='avatar' 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 className='acct' href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`}> + <Permalink className='acct' href={this.props.account.get('url')} to={`/@${this.props.account.get('acct')}`}> <strong>@{this.props.account.get('acct')}</strong> </Permalink> diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js index 98b48cd90..202d96676 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js @@ -104,7 +104,7 @@ class Conversation extends ImmutablePureComponent { markRead(); } - this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); + this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); } handleMarkAsRead = () => { @@ -163,7 +163,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/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index 9fe84c10b..2a3fd1ecf 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/follow_recommendations/components/account.js b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js index 046d03a9b..2c668da3e 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js +++ b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.js @@ -66,7 +66,7 @@ class Account extends ImmutablePureComponent { return ( <div className='account follow-recommendations-account'> <div className='account__wrapper'> - <Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <Permalink className='account__display-name account__display-name--with-note' 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} /> diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.js b/app/javascript/flavours/glitch/features/follow_recommendations/index.js index fee4d8757..d050e3cc7 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/index.js +++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.js @@ -68,7 +68,7 @@ class FollowRecommendations extends ImmutablePureComponent { } })); - router.history.push('/timelines/home'); + router.history.push('/home'); } render () { diff --git a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js index eb9f3db7e..cbe7a1032 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index 25f7f05b4..978436dcc 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { + lookupAccount, fetchAccount, fetchFollowers, expandFollowers, @@ -19,14 +20,25 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import TimelineHint from 'flavours/glitch/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), -}); +const mapStateToProps = (state, { params: { acct, id } }) => { + const accountId = id || 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), + }; +}; const RemoteHint = ({ url }) => ( <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} /> @@ -40,7 +52,11 @@ export default @connect(mapStateToProps) class Followers extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, @@ -51,32 +67,45 @@ 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, isAccount, dispatch } = this.props; + + if (!isAccount) dispatch(fetchAccount(accountId)); + dispatch(fetchFollowers(accountId)); } - 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)); + componentDidMount () { + const { params: { acct }, accountId, dispatch } = this.props; + + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); } } - handleHeaderClick = () => { - this.column.scrollTop(); + 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 }); setRef = c => { this.column = c; } + handleHeaderClick = () => { + this.column.scrollTop(); + } + render () { const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props; @@ -115,7 +144,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/flavours/glitch/features/following/index.js b/app/javascript/flavours/glitch/features/following/index.js index 968829fd5..446a19894 100644 --- a/app/javascript/flavours/glitch/features/following/index.js +++ b/app/javascript/flavours/glitch/features/following/index.js @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import { + lookupAccount, fetchAccount, fetchFollowing, expandFollowing, @@ -19,14 +20,25 @@ import MissingIndicator from 'flavours/glitch/components/missing_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import TimelineHint from 'flavours/glitch/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), -}); +const mapStateToProps = (state, { params: { acct, id } }) => { + const accountId = id || 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), + }; +}; const RemoteHint = ({ url }) => ( <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} /> @@ -40,7 +52,11 @@ export default @connect(mapStateToProps) class Following extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, @@ -51,32 +67,45 @@ 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, isAccount, dispatch } = this.props; + + if (!isAccount) dispatch(fetchAccount(accountId)); + dispatch(fetchFollowing(accountId)); } - 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)); + componentDidMount () { + const { params: { acct }, accountId, dispatch } = this.props; + + if (accountId) { + this._load(); + } else { + dispatch(lookupAccount(acct)); } } - handleHeaderClick = () => { - this.column.scrollTop(); + 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 }); setRef = c => { this.column = c; } + handleHeaderClick = () => { + this.column.scrollTop(); + } + render () { const { accountIds, hasMore, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props; @@ -115,7 +144,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/flavours/glitch/features/getting_started/components/announcements.js b/app/javascript/flavours/glitch/features/getting_started/components/announcements.js index 4eebe83ef..2f6d2de5c 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index b4549fdf8..56750fd88 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -107,7 +107,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); 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; } @@ -122,7 +122,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); if (multiColumn) { if (!columns.find(item => item.get('id') === 'HOME')) { - navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/timelines/home' />); + navItems.push(<ColumnLink key='0' icon='home' text={intl.formatMessage(messages.home_timeline)} to='/home' />); } if (!columns.find(item => item.get('id') === 'NOTIFICATIONS')) { @@ -130,16 +130,16 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); } if (!columns.find(item => item.get('id') === 'COMMUNITY')) { - navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />); + navItems.push(<ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/public/local' />); } if (!columns.find(item => item.get('id') === 'PUBLIC')) { - navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />); + navItems.push(<ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />); } } if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) { - navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />); + navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/conversations' />); } if (!multiColumn || !columns.find(item => item.get('id') === 'BOOKMARKS')) { @@ -160,7 +160,7 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2); <div key='9'> <ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' /> {lists.filter(list => !columns.find(item => item.get('id') === 'LIST' && item.getIn(['params', 'id']) === list.get('id'))).map(list => - <ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> + <ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/lists/${list.get('id')}`} icon='list-ul' text={list.get('title')} /> )} </div>, ]); diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js index e384f301b..b92389d82 100644 --- a/app/javascript/flavours/glitch/features/lists/index.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js index 0d3162fc9..b8fad19d0 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js @@ -39,7 +39,7 @@ export default class NotificationFollow 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 => { @@ -70,7 +70,7 @@ export default class NotificationFollow extends ImmutablePureComponent { className='notification__display-name' href={account.get('url')} title={account.get('acct')} - to={`/accounts/${account.get('id')}`} + to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={{ __html: displayName }} /></bdi> ); diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.js b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js index f351c1035..69b92a06f 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow_request.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.js @@ -45,7 +45,7 @@ class FollowRequest 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 => { @@ -89,7 +89,7 @@ class FollowRequest extends ImmutablePureComponent { className='notification__display-name' href={account.get('url')} title={account.get('acct')} - to={`/accounts/${account.get('id')}`} + to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={{ __html: displayName }} /></bdi> ); @@ -111,7 +111,7 @@ class FollowRequest extends ImmutablePureComponent { <div className='account'> <div className='account__wrapper'> - <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/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/flavours/glitch/features/picture_in_picture/components/footer.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js index 98d1f40b2..e01d277a1 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.js @@ -122,7 +122,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/flavours/glitch/features/picture_in_picture/components/header.js b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js index 28526ca88..26f2da374 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 416a4ca13..a1f981484 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -51,7 +51,7 @@ export default class DetailedStatus extends ImmutablePureComponent { e.preventDefault(); 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); } e.stopPropagation(); @@ -216,7 +216,7 @@ export default 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')} /> @@ -240,7 +240,7 @@ export default 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/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 9dbba4772..bddf1a2ef 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -405,7 +405,7 @@ class Status extends ImmutablePureComponent { 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); } handleMoveUp = id => { diff --git a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js index c4af25599..c23aec5ee 100644 --- a/app/javascript/flavours/glitch/features/ui/components/boost_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/boost_modal.js @@ -69,7 +69,7 @@ class BoostModal extends ImmutablePureComponent { this.props.onClose(); 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); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index d4e0bedac..e39a31e5d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -47,7 +47,7 @@ const componentMap = { 'DIRECTORY': Directory, }; -const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started|^\/start/); +const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/search|^\/getting-started|^\/start/); const messages = defineMessages({ publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, @@ -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={!swipeToChangeColumns}> diff --git a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js index ea1d7876e..9b7f9d1fb 100644 --- a/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/favourite_modal.js @@ -49,7 +49,7 @@ class FavouriteModal extends ImmutablePureComponent { this.props.onClose(); 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); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js index 354e35027..e61234283 100644 --- a/app/javascript/flavours/glitch/features/ui/components/list_panel.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index a8cbb837e..6974aab26 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -112,15 +112,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}`); - } - - this._sendBackgroundColor(); - } - componentDidUpdate (prevProps, prevState) { if (prevState.index !== this.state.index) { this._sendBackgroundColor(); diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js index 50e7d5c48..2dcd535ca 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.js @@ -11,12 +11,12 @@ import TrendsContainer from 'flavours/glitch/features/getting_started/containers const NavigationPanel = ({ onOpenSettings }) => ( <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='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></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/flavours/glitch/features/ui/components/onboarding_modal.js b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js index e0872927a..e7be62ad8 100644 --- a/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js @@ -79,7 +79,7 @@ const PageThree = ({ intl, myAccount }) => ( </div> </div> - <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/timelines/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/timelines/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p> + <p><FormattedMessage id='onboarding.page_three.search' defaultMessage='Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.' values={{ illustration: <Permalink to='/tag/illustration' href='/tags/illustration'>#illustration</Permalink>, introductions: <Permalink to='/tag/introductions' href='/tags/introductions'>#introductions</Permalink> }} /></p> <p><FormattedMessage id='onboarding.page_three.profile' defaultMessage='Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.' /></p> </div> ); @@ -130,7 +130,7 @@ const PageSix = ({ admin, domain }) => { if (admin) { adminSection = ( <p> - <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/accounts/${admin.get('id')}`}>@{admin.get('acct')}</Permalink> }} /> + <FormattedMessage id='onboarding.page_six.admin' defaultMessage="Your instance's admin is {admin}." values={{ admin: <Permalink href={admin.get('url')} to={`/@${admin.get('acct')}`}>@{admin.get('acct')}</Permalink> }} /> <br /> <FormattedMessage id='onboarding.page_six.read_guidelines' defaultMessage="Please read {domain}'s {guidelines}!" values={{ domain, guidelines: <a href='/about/more' target='_blank'><FormattedMessage id='onboarding.page_six.guidelines' defaultMessage='community guidelines' /></a> }} /> </p> diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js index a67405215..55cc84f5e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -8,10 +8,10 @@ import Icon from 'flavours/glitch/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/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index ad063f01b..7ca1adf7c 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -78,6 +78,7 @@ const mapStateToProps = state => ({ hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']), moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]), firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, + username: state.getIn(['accounts', me, 'username']), }); const keyMap = { @@ -190,7 +191,7 @@ class SwitchingColumnsArea extends React.PureComponent { render () { const { children, navbarUnder } = this.props; const singleColumn = this.state.mobile; - const redirect = singleColumn ? <Redirect from='/' to='/timelines/home' exact /> : <Redirect from='/' to='/getting-started' exact />; + const redirect = singleColumn ? <Redirect from='/' to='/home' exact /> : <Redirect from='/' to='/getting-started' exact />; return ( <ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn} navbarUnder={navbarUnder}> @@ -198,33 +199,40 @@ 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', '/timelines/home']} component={HomeTimeline} content={children} /> + <WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} /> + <WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} /> + <WrappedRoute path={['/conversations', '/timelines/direct']} 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='/statuses/new' component={Compose} content={children} /> + <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} /> + + <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} /> + <WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> + <WrappedRoute path={['/@:acct/followers', '/accounts/:id/followers']} component={Followers} content={children} /> + <WrappedRoute path={['/@:acct/following', '/accounts/:id/following']} component={Following} content={children} /> + <WrappedRoute path={['/@:acct/media', '/accounts/:id/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} /> + + {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} + <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> + <WrappedRoute path='/timelines/list/:id' component={ListTimeline} 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='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> @@ -265,6 +273,7 @@ class UI extends React.Component { showFaviconBadge: PropTypes.bool, moved: PropTypes.map, firstLaunch: PropTypes.bool, + username: PropTypes.string, }; state = { @@ -517,7 +526,7 @@ class UI extends React.Component { } handleHotkeyGoToHome = () => { - this.props.history.push('/timelines/home'); + this.props.history.push('/home'); } handleHotkeyGoToNotifications = () => { @@ -525,15 +534,15 @@ class UI extends React.Component { } handleHotkeyGoToLocal = () => { - this.props.history.push('/timelines/public/local'); + this.props.history.push('/public/local'); } handleHotkeyGoToFederated = () => { - this.props.history.push('/timelines/public'); + this.props.history.push('/public'); } handleHotkeyGoToDirect = () => { - this.props.history.push('/timelines/direct'); + this.props.history.push('/conversations'); } handleHotkeyGoToStart = () => { @@ -549,7 +558,7 @@ class UI extends React.Component { } handleHotkeyGoToProfile = () => { - this.props.history.push(`/accounts/${me}`); + this.props.history.push(`/@${this.props.username}`); } handleHotkeyGoToBlocked = () => { @@ -616,7 +625,7 @@ class UI extends React.Component { id='moved_to_warning' defaultMessage='This account is marked as moved to {moved_to_link}, and may thus not accept new follows.' values={{ moved_to_link: ( - <PermaLink href={moved.get('url')} to={`/accounts/${moved.get('id')}`}> + <PermaLink href={moved.get('url')} to={`/@${moved.get('acct')}`}> @{moved.get('acct')} </PermaLink> )}} diff --git a/app/javascript/flavours/glitch/reducers/accounts_map.js b/app/javascript/flavours/glitch/reducers/accounts_map.js new file mode 100644 index 000000000..e0d42e9cd --- /dev/null +++ b/app/javascript/flavours/glitch/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/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index c452e834c..7d7fe6fd3 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -40,6 +40,7 @@ import announcements from './announcements'; import markers from './markers'; import account_notes from './account_notes'; import picture_in_picture from './picture_in_picture'; +import accounts_map from './accounts_map'; const reducers = { announcements, @@ -54,6 +55,7 @@ const reducers = { status_lists, accounts, accounts_counters, + accounts_map, statuses, relationships, settings, diff --git a/app/javascript/flavours/glitch/util/api.js b/app/javascript/flavours/glitch/util/api.js index c59a24518..90d8465ef 100644 --- a/app/javascript/flavours/glitch/util/api.js +++ b/app/javascript/flavours/glitch/util/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/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..40d566d24 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' || routerHistory.location.pathname === '/statuses/new') && 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..cc0bfa9ba 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, fetchAccount } 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, id } }) => { + const accountId = id || 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,11 @@ export default @connect(mapStateToProps) class AccountGallery extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, @@ -67,15 +82,30 @@ class AccountGallery extends ImmutablePureComponent { width: 323, }; + _load () { + const { accountId, isAccount, dispatch } = this.props; + + if (!isAccount) dispatch(fetchAccount(accountId)); + 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 +125,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 +195,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..20f1dba9f 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, id }, withReplies = false }) => { + const accountId = id || 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,11 @@ export default @connect(mapStateToProps) class AccountTimeline extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, statusIds: ImmutablePropTypes.list, featuredStatusIds: ImmutablePropTypes.list, @@ -63,8 +76,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 +93,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 +126,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 +168,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..77ff2ce7b 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -81,7 +81,7 @@ class Conversation extends ImmutablePureComponent { markRead(); } - this.context.router.history.push(`/statuses/${lastStatus.get('id')}`); + this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`); } handleMarkAsRead = () => { @@ -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_recommendations/components/account.js b/app/javascript/mastodon/features/follow_recommendations/components/account.js index bd855aab0..ffc0ab00c 100644 --- a/app/javascript/mastodon/features/follow_recommendations/components/account.js +++ b/app/javascript/mastodon/features/follow_recommendations/components/account.js @@ -66,7 +66,7 @@ class Account extends ImmutablePureComponent { return ( <div className='account follow-recommendations-account'> <div className='account__wrapper'> - <Permalink className='account__display-name account__display-name--with-note' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}> + <Permalink className='account__display-name account__display-name--with-note' 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} /> diff --git a/app/javascript/mastodon/features/follow_recommendations/index.js b/app/javascript/mastodon/features/follow_recommendations/index.js index 26c8b2471..b5a71aef5 100644 --- a/app/javascript/mastodon/features/follow_recommendations/index.js +++ b/app/javascript/mastodon/features/follow_recommendations/index.js @@ -68,7 +68,7 @@ class FollowRecommendations extends ImmutablePureComponent { } })); - router.history.push('/timelines/home'); + router.history.push('/home'); } render () { 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..224e74b3d 100644 --- a/app/javascript/mastodon/features/followers/index.js +++ b/app/javascript/mastodon/features/followers/index.js @@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; import LoadingIndicator from '../../components/loading_indicator'; import { + lookupAccount, fetchAccount, fetchFollowers, expandFollowers, @@ -19,15 +20,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, id } }) => { + const accountId = id || 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 +53,11 @@ export default @connect(mapStateToProps) class Followers extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, @@ -53,22 +69,35 @@ 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, isAccount, dispatch } = this.props; + + if (!isAccount) dispatch(fetchAccount(accountId)); + 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 +140,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..aadce1644 100644 --- a/app/javascript/mastodon/features/following/index.js +++ b/app/javascript/mastodon/features/following/index.js @@ -6,6 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; import LoadingIndicator from '../../components/loading_indicator'; import { + lookupAccount, fetchAccount, fetchFollowing, expandFollowing, @@ -19,15 +20,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, id } }) => { + const accountId = id || 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 +53,11 @@ export default @connect(mapStateToProps) class Following extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.shape({ + acct: PropTypes.string, + id: PropTypes.string, + }).isRequired, + accountId: PropTypes.string, dispatch: PropTypes.func.isRequired, accountIds: ImmutablePropTypes.list, hasMore: PropTypes.bool, @@ -53,22 +69,35 @@ 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, isAccount, dispatch } = this.props; + + if (!isAccount) dispatch(fetchAccount(accountId)); + 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 +140,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..193637113 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -53,7 +53,7 @@ const messages = defineMessages({ publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, }); -const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started|^\/start/); +const shouldHideFAB = path => path.match(/^\/statuses\/|^\/@[^/]+\/\d+|^\/publish|^\/search|^\/getting-started|^\/start/); export default @(component => injectIntl(component, { withRef: true })) class ColumnsArea extends ImmutablePureComponent { @@ -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..3feffa656 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,33 +153,40 @@ 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', '/timelines/home']} component={HomeTimeline} content={children} /> + <WrappedRoute path={['/public', '/timelines/public']} exact component={PublicTimeline} content={children} /> + <WrappedRoute path={['/public/local', '/timelines/public/local']} exact component={CommunityTimeline} content={children} /> + <WrappedRoute path={['/conversations', '/timelines/direct']} 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='/statuses/new' component={Compose} content={children} /> + <WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} /> + + <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} /> + <WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> + <WrappedRoute path={['/@:acct/followers', '/accounts/:id/followers']} component={Followers} content={children} /> + <WrappedRoute path={['/@:acct/following', '/accounts/:id/following']} component={Following} content={children} /> + <WrappedRoute path={['/@:acct/media', '/accounts/:id/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} /> + + {/* Legacy routes, cannot be easily factored with other routes because they share a param name */} + <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> + <WrappedRoute path='/timelines/list/:id' component={ListTimeline} 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='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> @@ -214,6 +222,7 @@ class UI extends React.PureComponent { dropdownMenuIsOpen: PropTypes.bool, layout: PropTypes.string.isRequired, firstLaunch: PropTypes.bool, + username: PropTypes.string, }; state = { @@ -451,7 +460,7 @@ class UI extends React.PureComponent { } handleHotkeyGoToHome = () => { - this.context.router.history.push('/timelines/home'); + this.context.router.history.push('/home'); } handleHotkeyGoToNotifications = () => { @@ -459,15 +468,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 +492,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 |