Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `streaming/index.js`:
  Filtering code for streaming notifications has been refactored upstream, but
  glitch-soc had similar code for local-only toots in the same places.
  Ported upstream changes, but did not refactor local-only filtering.
master
Claire 2021-09-26 18:28:59 +02:00
commit 3622110778
47 changed files with 549 additions and 266 deletions

View File

@ -16,30 +16,7 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks! def redirect_unauthenticated_to_permalinks!
return if user_signed_in? return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/) redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
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)
end end
def set_pack def set_pack

View File

@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; 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_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; 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) { export function fetchAccountRequest(id) {
return { return {
type: ACCOUNT_FETCH_REQUEST, type: ACCOUNT_FETCH_REQUEST,

View File

@ -78,7 +78,7 @@ const COMPOSE_PANEL_BREAKPOINT = 600 + (285 * 1) + (10 * 1);
export const ensureComposeIsVisible = (getState, routerHistory) => { export const ensureComposeIsVisible = (getState, routerHistory) => {
if (!getState().getIn(['compose', 'mounted']) && window.innerWidth < COMPOSE_PANEL_BREAKPOINT) { 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']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
}, },
}).then(function (response) { }).then(function (response) {
if (routerHistory && routerHistory.location.pathname === '/statuses/new' && window.history.state) { if (routerHistory && routerHistory.location.pathname === '/publish' && window.history.state) {
routerHistory.goBack(); routerHistory.goBack();
} }

View File

@ -12,21 +12,35 @@ export const getLinks = response => {
return LinkHeader.parse(value); return LinkHeader.parse(value);
}; };
let csrfHeader = {}; const csrfHeader = {};
function setCSRFHeader() { const setCSRFHeader = () => {
const csrfToken = document.querySelector('meta[name=csrf-token]'); const csrfToken = document.querySelector('meta[name=csrf-token]');
if (csrfToken) { if (csrfToken) {
csrfHeader['X-CSRF-Token'] = csrfToken.content; csrfHeader['X-CSRF-Token'] = csrfToken.content;
} }
} };
ready(setCSRFHeader); 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({ export default getState => axios.create({
headers: Object.assign(csrfHeader, getState ? { headers: {
'Authorization': `Bearer ${getState().getIn(['meta', 'access_token'], '')}`, ...csrfHeader,
} : {}), ...authorizationHeaderFromState(getState),
},
transformResponse: [function (data) { transformResponse: [function (data) {
try { try {

View File

@ -118,7 +118,7 @@ class Account extends ImmutablePureComponent {
return ( return (
<div className='account'> <div className='account'>
<div className='account__wrapper'> <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> <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
{mute_expires_at} {mute_expires_at}
<DisplayName account={account} /> <DisplayName account={account} />

View File

@ -52,7 +52,7 @@ const Hashtag = ({ hashtag }) => (
<div className='trends__item__name'> <div className='trends__item__name'>
<Permalink <Permalink
href={hashtag.get('url')} href={hashtag.get('url')}
to={`/timelines/tag/${hashtag.get('name')}`} to={`/tags/${hashtag.get('name')}`}
> >
#<span>{hashtag.get('name')}</span> #<span>{hashtag.get('name')}</span>
</Permalink> </Permalink>

View File

@ -134,42 +134,28 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia }); this.setState({ showMedia: !this.state.showMedia });
} }
handleClick = () => { handleClick = e => {
if (this.props.onClick) { if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
this.props.onClick();
return; return;
} }
if (!this.context.router) { if (e) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
handleExpandClick = (e) => {
if (this.props.onClick) {
this.props.onClick();
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');
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
} }
this.handleHotkeyOpen();
}
handleAccountClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
}
if (e) {
e.preventDefault();
}
this.handleHotkeyOpenProfile();
} }
handleExpandedToggle = () => { handleExpandedToggle = () => {
@ -242,11 +228,30 @@ class Status extends ImmutablePureComponent {
} }
handleHotkeyOpen = () => { 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 = () => { 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 => { handleHotkeyMoveUp = e => {
@ -465,14 +470,15 @@ class Status extends ImmutablePureComponent {
{prepend} {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={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'> <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> <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} /> <RelativeTimestamp timestamp={status.get('created_at')} />
</a> </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'> <div className='status__avatar'>
{statusAvatar} {statusAvatar}
</div> </div>

View File

@ -186,7 +186,7 @@ class StatusActionBar extends ImmutablePureComponent {
} }
handleOpen = () => { 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 = () => { handleEmbed = () => {

View File

@ -112,7 +112,7 @@ export default class StatusContent extends React.PureComponent {
onMentionClick = (mention, e) => { onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); 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)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); 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 = ''; let mentionsPlaceholder = '';
const mentionLinks = status.get('mentions').map(item => ( 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> @<span>{item.get('username')}</span>
</Permalink> </Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []); )).reduce((aggregate, item) => [...aggregate, item, ' '], []);

View File

@ -22,14 +22,38 @@ const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction); store.dispatch(hydrateAction);
store.dispatch(fetchCustomEmojis()); 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 { export default class Mastodon extends React.PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired, 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() { componentDidMount() {
this.disconnect = store.dispatch(connectUserStream()); if (this.identity.signedIn) {
this.disconnect = store.dispatch(connectUserStream());
}
} }
componentWillUnmount () { componentWillUnmount () {

View File

@ -332,21 +332,21 @@ class Header extends ImmutablePureComponent {
{!suspended && ( {!suspended && (
<div className='account__header__extra__links'> <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 <ShortNumber
value={account.get('statuses_count')} value={account.get('statuses_count')}
renderer={counterRenderer('statuses')} renderer={counterRenderer('statuses')}
/> />
</NavLink> </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 <ShortNumber
value={account.get('following_count')} value={account.get('following_count')}
renderer={counterRenderer('following')} renderer={counterRenderer('following')}
/> />
</NavLink> </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 <ShortNumber
value={account.get('followers_count')} value={account.get('followers_count')}
renderer={counterRenderer('followers')} renderer={counterRenderer('followers')}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fetchAccount } from 'mastodon/actions/accounts'; import { lookupAccount } from 'mastodon/actions/accounts';
import { expandAccountMediaTimeline } from '../../actions/timelines'; import { expandAccountMediaTimeline } from '../../actions/timelines';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import LoadingIndicator from 'mastodon/components/loading_indicator';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
@ -17,14 +17,25 @@ import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { acct } }) => {
isAccount: !!state.getIn(['accounts', props.params.accountId]), const accountId = state.getIn(['accounts_map', acct]);
attachments: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), if (!accountId) {
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), return {
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false), isLoading: true,
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), };
}); }
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 { class LoadMoreMedia extends ImmutablePureComponent {
@ -52,7 +63,10 @@ export default @connect(mapStateToProps)
class AccountGallery extends ImmutablePureComponent { class AccountGallery extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string.isRequired,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
attachments: ImmutablePropTypes.list.isRequired, attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
@ -67,15 +81,29 @@ class AccountGallery extends ImmutablePureComponent {
width: 323, width: 323,
}; };
componentDidMount () { _load () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); const { accountId, dispatch } = this.props;
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
dispatch(expandAccountMediaTimeline(accountId));
} }
componentWillReceiveProps (nextProps) { componentDidMount () {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); 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));
} }
} }
@ -95,7 +123,7 @@ class AccountGallery extends ImmutablePureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
}; };
handleLoadOlder = e => { handleLoadOlder = e => {
@ -165,7 +193,7 @@ class AccountGallery extends ImmutablePureComponent {
<ScrollContainer scrollKey='account_gallery'> <ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} /> <HeaderContainer accountId={this.props.accountId} />
{(suspended || blockedBy) ? ( {(suspended || blockedBy) ? (
<div className='empty-column-indicator'> <div className='empty-column-indicator'>

View File

@ -123,9 +123,9 @@ export default class Header extends ImmutablePureComponent {
{!hideTabs && ( {!hideTabs && (
<div className='account__section-headline'> <div className='account__section-headline'>
<NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> <NavLink exact to={`/@${account.get('acct')}`}><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={`/@${account.get('acct')}/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')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div> </div>
)} )}
</div> </div>

View File

@ -21,7 +21,7 @@ export default class MovedNote extends ImmutablePureComponent {
handleAccountClick = e => { handleAccountClick = e => {
if (e.button === 0) { if (e.button === 0) {
e.preventDefault(); 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(); e.stopPropagation();

View File

@ -2,7 +2,7 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { fetchAccount } from '../../actions/accounts'; import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
@ -20,10 +20,19 @@ import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines'
const emptyList = ImmutableList(); const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const mapStateToProps = (state, { params: { acct }, withReplies = false }) => {
const accountId = state.getIn(['accounts_map', acct]);
if (!accountId) {
return {
isLoading: true,
};
}
const path = withReplies ? `${accountId}:with_replies` : accountId; const path = withReplies ? `${accountId}:with_replies` : accountId;
return { return {
accountId,
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
remoteUrl: state.getIn(['accounts', accountId, 'url']), remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]), isAccount: !!state.getIn(['accounts', accountId]),
@ -48,7 +57,10 @@ export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent { class AccountTimeline extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string.isRequired,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list, featuredStatusIds: ImmutablePropTypes.list,
@ -63,8 +75,8 @@ class AccountTimeline extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { _load () {
const { params: { accountId }, withReplies, dispatch } = this.props; const { accountId, withReplies, dispatch } = this.props;
dispatch(fetchAccount(accountId)); dispatch(fetchAccount(accountId));
dispatch(fetchAccountIdentityProofs(accountId)); dispatch(fetchAccountIdentityProofs(accountId));
@ -80,29 +92,32 @@ class AccountTimeline extends ImmutablePureComponent {
} }
} }
componentWillReceiveProps (nextProps) { componentDidMount () {
const { dispatch } = this.props; const { params: { acct }, accountId, dispatch } = this.props;
if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { if (accountId) {
dispatch(fetchAccount(nextProps.params.accountId)); this._load();
dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); } else {
dispatch(lookupAccount(acct));
}
}
if (!nextProps.withReplies) { componentDidUpdate (prevProps) {
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); 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) { if (prevProps.accountId === me && accountId !== me) {
dispatch(connectTimeline(`account:${me}`));
} else if (this.props.params.accountId === me && nextProps.params.accountId !== me) {
dispatch(disconnectTimeline(`account:${me}`)); dispatch(disconnectTimeline(`account:${me}`));
} }
} }
componentWillUnmount () { componentWillUnmount () {
const { dispatch, params: { accountId } } = this.props; const { dispatch, accountId } = this.props;
if (accountId === me) { if (accountId === me) {
dispatch(disconnectTimeline(`account:${me}`)); dispatch(disconnectTimeline(`account:${me}`));
@ -110,7 +125,7 @@ class AccountTimeline extends ImmutablePureComponent {
} }
handleLoadMore = maxId => { 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 () { render () {
@ -152,7 +167,7 @@ class AccountTimeline extends ImmutablePureComponent {
<ColumnBackButton multiColumn={multiColumn} /> <ColumnBackButton multiColumn={multiColumn} />
<StatusList <StatusList
prepend={<HeaderContainer accountId={this.props.params.accountId} />} prepend={<HeaderContainer accountId={this.props.accountId} />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
scrollKey='account_timeline' scrollKey='account_timeline'

View File

@ -19,13 +19,13 @@ export default class NavigationBar extends ImmutablePureComponent {
render () { render () {
return ( return (
<div className='navigation-bar'> <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> <span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
<Avatar account={this.props.account} size={48} /> <Avatar account={this.props.account} size={48} />
</Permalink> </Permalink>
<div className='navigation-bar__profile'> <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> <strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
</Permalink> </Permalink>

View File

@ -32,7 +32,7 @@ class ReplyIndicator extends ImmutablePureComponent {
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); 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'])}`);
} }
} }

View File

@ -100,16 +100,16 @@ class Compose extends React.PureComponent {
<nav className='drawer__header'> <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> <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') && ( {!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') && ( {!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> <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') && ( {!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') && ( {!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='/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> <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>

View File

@ -133,7 +133,7 @@ class Conversation extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); 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 = { const handlers = {
reply: this.handleReply, reply: this.handleReply,

View File

@ -213,7 +213,7 @@ class AccountCard extends ImmutablePureComponent {
<Permalink <Permalink
className='directory__card__bar__name' className='directory__card__bar__name'
href={account.get('url')} href={account.get('url')}
to={`/accounts/${account.get('id')}`} to={`/@${account.get('acct')}`}
> >
<Avatar account={account} size={48} /> <Avatar account={account} size={48} />
<DisplayName account={account} /> <DisplayName account={account} />

View File

@ -30,7 +30,7 @@ class AccountAuthorize extends ImmutablePureComponent {
return ( return (
<div className='account-authorize__wrapper'> <div className='account-authorize__wrapper'>
<div className='account-authorize'> <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> <div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>

View File

@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { import {
fetchAccount, lookupAccount,
fetchFollowers, fetchFollowers,
expandFollowers, expandFollowers,
} from '../../actions/accounts'; } from '../../actions/accounts';
@ -19,15 +19,26 @@ import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint'; import TimelineHint from 'mastodon/components/timeline_hint';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { acct } }) => {
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])), const accountId = state.getIn(['accounts_map', acct]);
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
isAccount: !!state.getIn(['accounts', props.params.accountId]), if (!accountId) {
accountIds: state.getIn(['user_lists', 'followers', props.params.accountId, 'items']), return {
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']), isLoading: true,
isLoading: state.getIn(['user_lists', 'followers', props.params.accountId, 'isLoading'], true), };
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), }
});
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 }) => ( const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} /> <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
@ -41,7 +52,10 @@ export default @connect(mapStateToProps)
class Followers extends ImmutablePureComponent { class Followers extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string.isRequired,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
@ -53,22 +67,34 @@ class Followers extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { _load () {
if (!this.props.accountIds) { const { accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowers(this.props.params.accountId)); dispatch(fetchFollowers(accountId));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
} }
} }
componentWillReceiveProps (nextProps) { componentDidUpdate (prevProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowers(nextProps.params.accountId)); if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
} }
} }
handleLoadMore = debounce(() => { handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowers(this.props.params.accountId)); this.props.dispatch(expandFollowers(this.props.accountId));
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
@ -111,7 +137,7 @@ class Followers extends ImmutablePureComponent {
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { import {
fetchAccount, lookupAccount,
fetchFollowing, fetchFollowing,
expandFollowing, expandFollowing,
} from '../../actions/accounts'; } from '../../actions/accounts';
@ -19,15 +19,26 @@ import ScrollableList from '../../components/scrollable_list';
import MissingIndicator from 'mastodon/components/missing_indicator'; import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint'; import TimelineHint from 'mastodon/components/timeline_hint';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, { params: { acct } }) => {
remote: !!(state.getIn(['accounts', props.params.accountId, 'acct']) !== state.getIn(['accounts', props.params.accountId, 'username'])), const accountId = state.getIn(['accounts_map', acct]);
remoteUrl: state.getIn(['accounts', props.params.accountId, 'url']),
isAccount: !!state.getIn(['accounts', props.params.accountId]), if (!accountId) {
accountIds: state.getIn(['user_lists', 'following', props.params.accountId, 'items']), return {
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']), isLoading: true,
isLoading: state.getIn(['user_lists', 'following', props.params.accountId, 'isLoading'], true), };
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false), }
});
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 }) => ( const RemoteHint = ({ url }) => (
<TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} /> <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
@ -41,7 +52,10 @@ export default @connect(mapStateToProps)
class Following extends ImmutablePureComponent { class Following extends ImmutablePureComponent {
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.shape({
acct: PropTypes.string.isRequired,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list, accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
@ -53,22 +67,34 @@ class Following extends ImmutablePureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
}; };
componentWillMount () { _load () {
if (!this.props.accountIds) { const { accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(this.props.params.accountId));
this.props.dispatch(fetchFollowing(this.props.params.accountId)); dispatch(fetchFollowing(accountId));
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
} }
} }
componentWillReceiveProps (nextProps) { componentDidUpdate (prevProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(fetchAccount(nextProps.params.accountId));
this.props.dispatch(fetchFollowing(nextProps.params.accountId)); if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
} }
} }
handleLoadMore = debounce(() => { handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowing(this.props.params.accountId)); this.props.dispatch(expandFollowing(this.props.accountId));
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
@ -111,7 +137,7 @@ class Following extends ImmutablePureComponent {
hasMore={hasMore} hasMore={hasMore}
isLoading={isLoading} isLoading={isLoading}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />} prepend={<HeaderContainer accountId={this.props.accountId} hideTabs />}
alwaysPrepend alwaysPrepend
append={remoteMessage} append={remoteMessage}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}

View File

@ -87,7 +87,7 @@ class Content extends ImmutablePureComponent {
onMentionClick = (mention, e) => { onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); 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)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/timelines/tag/${hashtag}`); this.context.router.history.push(`/tags/${hashtag}`);
} }
} }
onStatusClick = (status, e) => { onStatusClick = (status, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.context.router.history.push(`/statuses/${status.get('id')}`); this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
} }
} }

View File

@ -82,7 +82,7 @@ class GettingStarted extends ImmutablePureComponent {
const { fetchFollowRequests, multiColumn } = this.props; const { fetchFollowRequests, multiColumn } = this.props;
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) { if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
this.context.router.history.replace('/timelines/home'); this.context.router.history.replace('/home');
return; return;
} }
@ -98,8 +98,8 @@ class GettingStarted extends ImmutablePureComponent {
if (multiColumn) { if (multiColumn) {
navItems.push( navItems.push(
<ColumnSubheading key='header-discover' text={intl.formatMessage(messages.discover)} />, <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='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='/timelines/public' />, <ColumnLink key='public_timeline' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/public' />,
); );
height += 34 + 48*2; height += 34 + 48*2;
@ -127,13 +127,13 @@ class GettingStarted extends ImmutablePureComponent {
if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) { if (multiColumn && !columns.find(item => item.get('id') === 'HOME')) {
navItems.push( 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; height += 48;
} }
navItems.push( 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='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <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' />, <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,

View File

@ -73,7 +73,7 @@ class Lists extends ImmutablePureComponent {
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
> >
{lists.map(list => {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> </ScrollableList>
</Column> </Column>

View File

@ -42,7 +42,7 @@ class FollowRequest extends ImmutablePureComponent {
return ( return (
<div className='account'> <div className='account'>
<div className='account__wrapper'> <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> <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} /> <DisplayName account={account} />
</Permalink> </Permalink>

View File

@ -68,7 +68,7 @@ class Notification extends ImmutablePureComponent {
const { notification } = this.props; const { notification } = this.props;
if (notification.get('status')) { 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 { } else {
this.handleOpenProfile(); this.handleOpenProfile();
} }
@ -76,7 +76,7 @@ class Notification extends ImmutablePureComponent {
handleOpenProfile = () => { handleOpenProfile = () => {
const { notification } = this.props; 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 => { handleMention = e => {
@ -315,7 +315,7 @@ class Notification extends ImmutablePureComponent {
const { notification } = this.props; const { notification } = this.props;
const account = notification.get('account'); const account = notification.get('account');
const displayNameHtml = { __html: account.get('display_name_html') }; 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')) { switch(notification.get('type')) {
case 'follow': case 'follow':

View File

@ -120,7 +120,7 @@ class Footer extends ImmutablePureComponent {
onClose(); onClose();
} }
router.history.push(`/statuses/${status.get('id')}`); router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
} }
render () { render () {

View File

@ -34,7 +34,7 @@ class Header extends ImmutablePureComponent {
return ( return (
<div className='picture-in-picture__header'> <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} /> <Avatar account={account} size={36} />
<DisplayName account={account} /> <DisplayName account={account} />
</Link> </Link>

View File

@ -55,7 +55,7 @@ class DetailedStatus extends ImmutablePureComponent {
handleAccountClick = (e) => { handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) { if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
e.preventDefault(); 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(); e.stopPropagation();
@ -195,7 +195,7 @@ class DetailedStatus extends ImmutablePureComponent {
reblogLink = ( reblogLink = (
<React.Fragment> <React.Fragment>
<React.Fragment> · </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} /> <Icon id={reblogIcon} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} /> <AnimatedNumber value={status.get('reblogs_count')} />
@ -219,7 +219,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (this.context.router) { if (this.context.router) {
favouriteLink = ( 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' /> <Icon id='star' />
<span className='detailed-status__favorites'> <span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} /> <AnimatedNumber value={status.get('favourites_count')} />

View File

@ -396,7 +396,7 @@ class Status extends ImmutablePureComponent {
} }
handleHotkeyOpenProfile = () => { 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 = () => { handleHotkeyToggleHidden = () => {

View File

@ -68,7 +68,7 @@ class BoostModal extends ImmutablePureComponent {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault(); e.preventDefault();
this.props.onClose(); 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'])}`);
} }
} }

View File

@ -216,7 +216,7 @@ class ColumnsArea extends ImmutablePureComponent {
const columnIndex = getIndex(this.context.router.history.location.pathname); const columnIndex = getIndex(this.context.router.history.location.pathname);
if (singleColumn) { 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 ? ( 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}> <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}>

View File

@ -46,7 +46,7 @@ class ListPanel extends ImmutablePureComponent {
<hr /> <hr />
{lists.map(list => ( {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> </div>
); );

View File

@ -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 () { render () {
const { media, statusId, intl, onClose } = this.props; const { media, statusId, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;

View File

@ -10,12 +10,12 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
const NavigationPanel = () => ( const NavigationPanel = () => (
<div className='navigation-panel'> <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> <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 /> <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' 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='/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' 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='/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='/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='/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='/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> <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>

View File

@ -8,10 +8,10 @@ import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon'; import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [ 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='/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' 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='/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' 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 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>, <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>,
]; ];

View File

@ -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, 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, dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION, firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
username: state.getIn(['accounts', me, 'username']),
}); });
const keyMap = { const keyMap = {
@ -144,7 +145,7 @@ class SwitchingColumnsArea extends React.PureComponent {
render () { render () {
const { children, mobile } = this.props; 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 ( return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}> <ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
@ -152,32 +153,32 @@ class SwitchingColumnsArea extends React.PureComponent {
{redirect} {redirect}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} /> <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} /> <WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' exact component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/public/local' exact component={CommunityTimeline} content={children} />
<WrappedRoute path='/conversations' component={DirectTimeline} content={children} />
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} /> <WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/start' component={FollowRecommendations} content={children} /> <WrappedRoute path='/start' component={FollowRecommendations} content={children} />
<WrappedRoute path='/search' component={Search} content={children} /> <WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} /> <WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path='/publish' component={Compose} content={children} />
<WrappedRoute path='/statuses/new' component={Compose} content={children} /> <WrappedRoute path='/@:acct' exact component={AccountTimeline} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/@:acct/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/@:acct/followers' component={Followers} content={children} />
<WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> <WrappedRoute path='/@:acct/following' component={Following} content={children} />
<WrappedRoute path='/@:acct/media' component={AccountGallery} content={children} />
<WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> <WrappedRoute path='/@:acct/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> <WrappedRoute path='/@:acct/:statusId/reblogs' component={Reblogs} content={children} />
<WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> <WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} 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='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} />
@ -214,6 +215,7 @@ class UI extends React.PureComponent {
dropdownMenuIsOpen: PropTypes.bool, dropdownMenuIsOpen: PropTypes.bool,
layout: PropTypes.string.isRequired, layout: PropTypes.string.isRequired,
firstLaunch: PropTypes.bool, firstLaunch: PropTypes.bool,
username: PropTypes.string,
}; };
state = { state = {
@ -451,7 +453,7 @@ class UI extends React.PureComponent {
} }
handleHotkeyGoToHome = () => { handleHotkeyGoToHome = () => {
this.context.router.history.push('/timelines/home'); this.context.router.history.push('/home');
} }
handleHotkeyGoToNotifications = () => { handleHotkeyGoToNotifications = () => {
@ -459,15 +461,15 @@ class UI extends React.PureComponent {
} }
handleHotkeyGoToLocal = () => { handleHotkeyGoToLocal = () => {
this.context.router.history.push('/timelines/public/local'); this.context.router.history.push('/public/local');
} }
handleHotkeyGoToFederated = () => { handleHotkeyGoToFederated = () => {
this.context.router.history.push('/timelines/public'); this.context.router.history.push('/public');
} }
handleHotkeyGoToDirect = () => { handleHotkeyGoToDirect = () => {
this.context.router.history.push('/timelines/direct'); this.context.router.history.push('/conversations');
} }
handleHotkeyGoToStart = () => { handleHotkeyGoToStart = () => {
@ -483,7 +485,7 @@ class UI extends React.PureComponent {
} }
handleHotkeyGoToProfile = () => { handleHotkeyGoToProfile = () => {
this.context.router.history.push(`/accounts/${me}`); this.context.router.history.push(`/@${this.props.username}`);
} }
handleHotkeyGoToBlocked = () => { handleHotkeyGoToBlocked = () => {

View File

@ -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;
}
};

View File

@ -38,6 +38,7 @@ import missed_updates from './missed_updates';
import announcements from './announcements'; import announcements from './announcements';
import markers from './markers'; import markers from './markers';
import picture_in_picture from './picture_in_picture'; import picture_in_picture from './picture_in_picture';
import accounts_map from './accounts_map';
const reducers = { const reducers = {
announcements, announcements,
@ -52,6 +53,7 @@ const reducers = {
status_lists, status_lists,
accounts, accounts,
accounts_counters, accounts_counters,
accounts_map,
statuses, statuses,
relationships, relationships,
settings, settings,

View File

@ -90,7 +90,7 @@ const handlePush = (event) => {
options.tag = notification.id; options.tag = notification.id;
options.badge = '/badge.png'; options.badge = '/badge.png';
options.image = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined; 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) { if (notification.status && notification.status.spoiler_text || notification.status.sensitive) {
options.data.hiddenBody = htmlToPlainText(notification.status.content); options.data.hiddenBody = htmlToPlainText(notification.status.content);

View File

@ -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

View File

@ -127,7 +127,7 @@ class NotifyService < BaseService
def push_notification! def push_notification!
return if @notification.activity.nil? 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! send_push_notifications!
end end

View File

@ -8,8 +8,10 @@ RSpec.describe HomeController, type: :controller do
context 'when not signed in' do context 'when not signed in' do
context 'when requested path is tag timeline' do context 'when requested path is tag timeline' do
before { @request.path = '/web/timelines/tag/name' } it 'redirects to the tag\'s permalink' do
it { is_expected.to redirect_to '/tags/name' } @request.path = '/web/timelines/tag/name'
is_expected.to redirect_to '/tags/name'
end
end end
it 'redirects to about page' do it 'redirects to about page' do

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'rails_helper'
describe PermalinkRedirector do
describe '#redirect_url' do
before do
account = Fabricate(:account, username: 'alice', id: 1)
Fabricate(:status, account: account, id: 123)
end
it 'returns path for legacy account links' do
redirector = described_class.new('web/accounts/1')
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice'
end
it 'returns path for legacy status links' do
redirector = described_class.new('web/statuses/123')
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123'
end
it 'returns path for legacy tag links' do
redirector = described_class.new('web/timelines/tag/hoge')
expect(redirector.redirect_path).to eq '/tags/hoge'
end
it 'returns path for pretty account links' do
redirector = described_class.new('web/@alice')
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice'
end
it 'returns path for pretty status links' do
redirector = described_class.new('web/@alice/123')
expect(redirector.redirect_path).to eq 'https://cb6e6126.ngrok.io/@alice/123'
end
it 'returns path for pretty tag links' do
redirector = described_class.new('web/tags/hoge')
expect(redirector.redirect_path).to eq '/tags/hoge'
end
end
end

View File

@ -282,6 +282,14 @@ const startWorker = (workerId) => {
next(); next();
}; };
/**
* @param {any} req
* @param {string[]} necessaryScopes
* @return {boolean}
*/
const isInScope = (req, necessaryScopes) =>
req.scopes.some(scope => necessaryScopes.includes(scope));
/** /**
* @param {string} token * @param {string} token
* @param {any} req * @param {any} req
@ -314,7 +322,6 @@ const startWorker = (workerId) => {
req.scopes = result.rows[0].scopes.split(' '); req.scopes = result.rows[0].scopes.split(' ');
req.accountId = result.rows[0].account_id; req.accountId = result.rows[0].account_id;
req.chosenLanguages = result.rows[0].chosen_languages; req.chosenLanguages = result.rows[0].chosen_languages;
req.allowNotifications = req.scopes.some(scope => ['read', 'read:notifications'].includes(scope));
req.deviceId = result.rows[0].device_id; req.deviceId = result.rows[0].device_id;
resolve(); resolve();
@ -581,15 +588,13 @@ const startWorker = (workerId) => {
* @param {function(string, string): void} output * @param {function(string, string): void} output
* @param {function(string[], function(string): void): void} attachCloseHandler * @param {function(string[], function(string): void): void} attachCloseHandler
* @param {boolean=} needsFiltering * @param {boolean=} needsFiltering
* @param {boolean=} notificationOnly
* @param {boolean=} allowLocalOnly * @param {boolean=} allowLocalOnly
* @return {function(string): void} * @return {function(string): void}
*/ */
const streamFrom = (ids, req, output, attachCloseHandler, needsFiltering = false, notificationOnly = false, allowLocalOnly = false) => { const streamFrom = (ids, req, output, attachCloseHandler, needsFiltering = false, allowLocalOnly = false) => {
const accountId = req.accountId || req.remoteAddress; const accountId = req.accountId || req.remoteAddress;
const streamType = notificationOnly ? ' (notification)' : '';
log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}${streamType}`); log.verbose(req.requestId, `Starting stream from ${ids.join(', ')} for ${accountId}`);
const listener = message => { const listener = message => {
const json = parseJSON(message); const json = parseJSON(message);
@ -607,14 +612,6 @@ const startWorker = (workerId) => {
output(event, encodedPayload); output(event, encodedPayload);
}; };
if (notificationOnly && event !== 'notification') {
return;
}
if (event === 'notification' && !req.allowNotifications) {
return;
}
// Only send local-only statuses to logged-in users // Only send local-only statuses to logged-in users
if (event === 'update' && payload.local_only && !(req.accountId && allowLocalOnly)) { if (event === 'update' && payload.local_only && !(req.accountId && allowLocalOnly)) {
log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`); log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`);
@ -767,7 +764,7 @@ const startWorker = (workerId) => {
const onSend = streamToHttp(req, res); const onSend = streamToHttp(req, res);
const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds)); const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds));
streamFrom(channelIds, req, onSend, onEnd, options.needsFiltering, options.notificationOnly, options.allowLocalOnly); streamFrom(channelIds, req, onSend, onEnd, options.needsFiltering, options.allowLocalOnly);
}).catch(err => { }).catch(err => {
log.verbose(req.requestId, 'Subscription error:', err.toString()); log.verbose(req.requestId, 'Subscription error:', err.toString());
httpNotFound(res); httpNotFound(res);
@ -783,88 +780,106 @@ const startWorker = (workerId) => {
* @property {string} [only_media] * @property {string} [only_media]
*/ */
/**
* @param {any} req
* @return {string[]}
*/
const channelsForUserStream = req => {
const arr = [`timeline:${req.accountId}`];
if (isInScope(req, ['crypto']) && req.deviceId) {
arr.push(`timeline:${req.accountId}:${req.deviceId}`);
}
if (isInScope(req, ['read', 'read:notifications'])) {
arr.push(`timeline:${req.accountId}:notifications`);
}
return arr;
};
/** /**
* @param {any} req * @param {any} req
* @param {string} name * @param {string} name
* @param {StreamParams} params * @param {StreamParams} params
* @return {Promise.<{ channelIds: string[], options: { needsFiltering: boolean, notificationOnly: boolean } }>} * @return {Promise.<{ channelIds: string[], options: { needsFiltering: boolean } }>}
*/ */
const channelNameToIds = (req, name, params) => new Promise((resolve, reject) => { const channelNameToIds = (req, name, params) => new Promise((resolve, reject) => {
switch(name) { switch(name) {
case 'user': case 'user':
resolve({ resolve({
channelIds: req.deviceId ? [`timeline:${req.accountId}`, `timeline:${req.accountId}:${req.deviceId}`] : [`timeline:${req.accountId}`], channelIds: channelsForUserStream(req),
options: { needsFiltering: false, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: false, allowLocalOnly: true },
}); });
break; break;
case 'user:notification': case 'user:notification':
resolve({ resolve({
channelIds: [`timeline:${req.accountId}`], channelIds: [`timeline:${req.accountId}:notifications`],
options: { needsFiltering: false, notificationOnly: true, allowLocalOnly: true }, options: { needsFiltering: false, allowLocalOnly: true },
}); });
break; break;
case 'public': case 'public':
resolve({ resolve({
channelIds: ['timeline:public'], channelIds: ['timeline:public'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: isTruthy(params.allow_local_only) }, options: { needsFiltering: true, allowLocalOnly: isTruthy(params.allow_local_only) },
}); });
break; break;
case 'public:allow_local_only': case 'public:allow_local_only':
resolve({ resolve({
channelIds: ['timeline:public'], channelIds: ['timeline:public'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
break; break;
case 'public:local': case 'public:local':
resolve({ resolve({
channelIds: ['timeline:public:local'], channelIds: ['timeline:public:local'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
break; break;
case 'public:remote': case 'public:remote':
resolve({ resolve({
channelIds: ['timeline:public:remote'], channelIds: ['timeline:public:remote'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: false }, options: { needsFiltering: true, allowLocalOnly: false },
}); });
break; break;
case 'public:media': case 'public:media':
resolve({ resolve({
channelIds: ['timeline:public:media'], channelIds: ['timeline:public:media'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: isTruthy(query.allow_local_only) }, options: { needsFiltering: true, allowLocalOnly: isTruthy(query.allow_local_only) },
}); });
break; break;
case 'public:allow_local_only:media': case 'public:allow_local_only:media':
resolve({ resolve({
channelIds: ['timeline:public:media'], channelIds: ['timeline:public:media'],
options: { needsFiltering: true, notificationsOnly: false, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
break; break;
case 'public:local:media': case 'public:local:media':
resolve({ resolve({
channelIds: ['timeline:public:local:media'], channelIds: ['timeline:public:local:media'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
break; break;
case 'public:remote:media': case 'public:remote:media':
resolve({ resolve({
channelIds: ['timeline:public:remote:media'], channelIds: ['timeline:public:remote:media'],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: false }, options: { needsFiltering: true, allowLocalOnly: false },
}); });
break; break;
case 'direct': case 'direct':
resolve({ resolve({
channelIds: [`timeline:direct:${req.accountId}`], channelIds: [`timeline:direct:${req.accountId}`],
options: { needsFiltering: false, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: false, allowLocalOnly: true },
}); });
break; break;
@ -874,7 +889,7 @@ const startWorker = (workerId) => {
} else { } else {
resolve({ resolve({
channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}`], channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}`],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
} }
@ -885,7 +900,7 @@ const startWorker = (workerId) => {
} else { } else {
resolve({ resolve({
channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}:local`], channelIds: [`timeline:hashtag:${params.tag.toLowerCase()}:local`],
options: { needsFiltering: true, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: true, allowLocalOnly: true },
}); });
} }
@ -894,7 +909,7 @@ const startWorker = (workerId) => {
authorizeListAccess(params.list, req).then(() => { authorizeListAccess(params.list, req).then(() => {
resolve({ resolve({
channelIds: [`timeline:list:${params.list}`], channelIds: [`timeline:list:${params.list}`],
options: { needsFiltering: false, notificationOnly: false, allowLocalOnly: true }, options: { needsFiltering: false, allowLocalOnly: true },
}); });
}).catch(() => { }).catch(() => {
reject('Not authorized to stream this list'); reject('Not authorized to stream this list');
@ -941,7 +956,8 @@ const startWorker = (workerId) => {
const onSend = streamToWs(request, socket, streamNameFromChannelName(channelName, params)); const onSend = streamToWs(request, socket, streamNameFromChannelName(channelName, params));
const stopHeartbeat = subscriptionHeartbeat(channelIds); const stopHeartbeat = subscriptionHeartbeat(channelIds);
const listener = streamFrom(channelIds, request, onSend, undefined, options.needsFiltering, options.notificationOnly, options.allowLocalOnly);
const listener = streamFrom(channelIds, request, onSend, undefined, options.needsFiltering, options.allowLocalOnly);
subscriptions[channelIds.join(';')] = { subscriptions[channelIds.join(';')] = {
listener, listener,