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.
main
Claire 3 years ago
commit 3622110778

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

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

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

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

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

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

@ -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;
}
if (!this.context.router) {
return; return;
} }
const { status } = this.props; if (e) {
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`); e.preventDefault();
} }
handleExpandClick = (e) => { this.handleHotkeyOpen();
if (this.props.onClick) {
this.props.onClick();
return;
} }
if (e.button === 0) { handleAccountClick = e => {
if (!this.context.router) { if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return; return;
} }
const { status } = this.props; if (e) {
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.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>

@ -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 = () => {

@ -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, ' '], []);

@ -22,15 +22,39 @@ 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() {
if (this.identity.signedIn) {
this.disconnect = store.dispatch(connectUserStream()); this.disconnect = store.dispatch(connectUserStream());
} }
}
componentWillUnmount () { componentWillUnmount () {
if (this.disconnect) { if (this.disconnect) {

@ -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')}

@ -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,
}; };
_load () {
const { accountId, dispatch } = this.props;
dispatch(expandAccountMediaTimeline(accountId));
}
componentDidMount () { componentDidMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId)); const { params: { acct }, accountId, dispatch } = this.props;
this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
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(expandAccountMediaTimeline(this.props.params.accountId)); 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'>

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

@ -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();

@ -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) { }
dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId));
} }
dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); 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));
} }
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'

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

@ -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'])}`);
} }
} }

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

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

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

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

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

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

@ -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')}`);
} }
} }

@ -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' />,

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

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

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

@ -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 () {

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

@ -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')} />

@ -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 = () => {

@ -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'])}`);
} }
} }

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

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

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

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

@ -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>,
]; ];

@ -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 = () => {

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

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

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

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

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

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

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

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

Loading…
Cancel
Save