Change routing paths to use usernames in web UI (#16171)

master
Eugen Rochko 3 years ago committed by GitHub
parent 9c92907681
commit 52e5c07948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      app/controllers/home_controller.rb
  2. 32
      app/javascript/mastodon/actions/accounts.js
  3. 4
      app/javascript/mastodon/actions/compose.js
  4. 26
      app/javascript/mastodon/api.js
  5. 2
      app/javascript/mastodon/components/account.js
  6. 2
      app/javascript/mastodon/components/hashtag.js
  7. 64
      app/javascript/mastodon/components/status.js
  8. 2
      app/javascript/mastodon/components/status_action_bar.js
  9. 6
      app/javascript/mastodon/components/status_content.js
  10. 26
      app/javascript/mastodon/containers/mastodon.js
  11. 6
      app/javascript/mastodon/features/account/components/header.js
  12. 64
      app/javascript/mastodon/features/account_gallery/index.js
  13. 6
      app/javascript/mastodon/features/account_timeline/components/header.js
  14. 2
      app/javascript/mastodon/features/account_timeline/components/moved_note.js
  15. 55
      app/javascript/mastodon/features/account_timeline/index.js
  16. 4
      app/javascript/mastodon/features/compose/components/navigation_bar.js
  17. 2
      app/javascript/mastodon/features/compose/components/reply_indicator.js
  18. 6
      app/javascript/mastodon/features/compose/index.js
  19. 2
      app/javascript/mastodon/features/direct_timeline/components/conversation.js
  20. 2
      app/javascript/mastodon/features/directory/components/account_card.js
  21. 2
      app/javascript/mastodon/features/follow_requests/components/account_authorize.js
  22. 68
      app/javascript/mastodon/features/followers/index.js
  23. 68
      app/javascript/mastodon/features/following/index.js
  24. 6
      app/javascript/mastodon/features/getting_started/components/announcements.js
  25. 10
      app/javascript/mastodon/features/getting_started/index.js
  26. 2
      app/javascript/mastodon/features/lists/index.js
  27. 2
      app/javascript/mastodon/features/notifications/components/follow_request.js
  28. 6
      app/javascript/mastodon/features/notifications/components/notification.js
  29. 2
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  30. 2
      app/javascript/mastodon/features/picture_in_picture/components/header.js
  31. 6
      app/javascript/mastodon/features/status/components/detailed_status.js
  32. 2
      app/javascript/mastodon/features/status/index.js
  33. 2
      app/javascript/mastodon/features/ui/components/boost_modal.js
  34. 2
      app/javascript/mastodon/features/ui/components/columns_area.js
  35. 2
      app/javascript/mastodon/features/ui/components/list_panel.js
  36. 7
      app/javascript/mastodon/features/ui/components/media_modal.js
  37. 8
      app/javascript/mastodon/features/ui/components/navigation_panel.js
  38. 6
      app/javascript/mastodon/features/ui/components/tabs_bar.js
  39. 46
      app/javascript/mastodon/features/ui/index.js
  40. 15
      app/javascript/mastodon/reducers/accounts_map.js
  41. 2
      app/javascript/mastodon/reducers/index.js
  42. 2
      app/javascript/mastodon/service_worker/web_push_notifications.js
  43. 63
      app/lib/permalink_redirector.rb
  44. 6
      spec/controllers/home_controller_spec.rb
  45. 42
      spec/lib/permalink_redirector_spec.rb

@ -14,30 +14,7 @@ class HomeController < ApplicationController
def redirect_unauthenticated_to_permalinks!
return if user_signed_in?
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
if matches
case matches[1]
when 'statuses'
status = Status.find_by(id: matches[2])
if status&.distributable?
redirect_to(ActivityPub::TagManager.instance.url_for(status))
return
end
when 'accounts'
account = Account.find_by(id: matches[2])
if account
redirect_to(ActivityPub::TagManager.instance.url_for(account))
return
end
end
end
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path)
end
def default_redirect_path

@ -5,6 +5,10 @@ export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL';
export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST';
export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL';
export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST';
export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS';
export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL';
@ -87,6 +91,34 @@ export function fetchAccount(id) {
};
};
export const lookupAccount = acct => (dispatch, getState) => {
dispatch(lookupAccountRequest(acct));
api(getState).get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
dispatch(lookupAccountSuccess());
}).catch(error => {
dispatch(lookupAccountFail(acct, error));
});
};
export const lookupAccountRequest = (acct) => ({
type: ACCOUNT_LOOKUP_REQUEST,
acct,
});
export const lookupAccountSuccess = () => ({
type: ACCOUNT_LOOKUP_SUCCESS,
});
export const lookupAccountFail = (acct, error) => ({
type: ACCOUNT_LOOKUP_FAIL,
acct,
error,
skipAlert: true,
});
export function fetchAccountRequest(id) {
return {
type: ACCOUNT_FETCH_REQUEST,

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

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

@ -118,7 +118,7 @@ class Account extends ImmutablePureComponent {
return (
<div className='account'>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/accounts/${account.get('id')}`}>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
{mute_expires_at}
<DisplayName account={account} />

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

@ -134,42 +134,28 @@ class Status extends ImmutablePureComponent {
this.setState({ showMedia: !this.state.showMedia });
}
handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
handleClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
}
if (!this.context.router) {
return;
if (e) {
e.preventDefault();
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
this.handleHotkeyOpen();
}
handleExpandClick = (e) => {
if (this.props.onClick) {
this.props.onClick();
handleAccountClick = e => {
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
return;
}
if (e.button === 0) {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
}
handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
const id = e.currentTarget.getAttribute('data-id');
if (e) {
e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
}
this.handleHotkeyOpenProfile();
}
handleExpandedToggle = () => {
@ -242,11 +228,30 @@ class Status extends ImmutablePureComponent {
}
handleHotkeyOpen = () => {
this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
if (this.props.onClick) {
this.props.onClick();
return;
}
const { router } = this.context;
const status = this._properStatus();
if (!router) {
return;
}
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
}
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
const { router } = this.context;
const status = this._properStatus();
if (!router) {
return;
}
router.history.push(`/@${status.getIn(['account', 'acct'])}`);
}
handleHotkeyMoveUp = e => {
@ -465,14 +470,15 @@ class Status extends ImmutablePureComponent {
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__expand' onClick={this.handleClick} role='presentation' />
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>

@ -186,7 +186,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
handleOpen = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
}
handleEmbed = () => {

@ -112,7 +112,7 @@ export default class StatusContent extends React.PureComponent {
onMentionClick = (mention, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/accounts/${mention.get('id')}`);
this.context.router.history.push(`/@${mention.get('acct')}`);
}
}
@ -121,7 +121,7 @@ export default class StatusContent extends React.PureComponent {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/timelines/tag/${hashtag}`);
this.context.router.history.push(`/tags/${hashtag}`);
}
}
@ -198,7 +198,7 @@ export default class StatusContent extends React.PureComponent {
let mentionsPlaceholder = '';
const mentionLinks = status.get('mentions').map(item => (
<Permalink to={`/accounts/${item.get('id')}`} href={item.get('url')} key={item.get('id')} className='mention'>
<Permalink to={`/@${item.get('acct')}`} href={item.get('url')} key={item.get('id')} className='mention'>
@<span>{item.get('username')}</span>
</Permalink>
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);

@ -22,14 +22,38 @@ const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction);
store.dispatch(fetchCustomEmojis());
const createIdentityContext = state => ({
signedIn: !!state.meta.me,
accountId: state.meta.me,
accessToken: state.meta.access_token,
});
export default class Mastodon extends React.PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
static childContextTypes = {
identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired,
accountId: PropTypes.string,
accessToken: PropTypes.string,
}).isRequired,
};
identity = createIdentityContext(initialState);
getChildContext() {
return {
identity: this.identity,
};
}
componentDidMount() {
this.disconnect = store.dispatch(connectUserStream());
if (this.identity.signedIn) {
this.disconnect = store.dispatch(connectUserStream());
}
}
componentWillUnmount () {

@ -332,21 +332,21 @@ class Header extends ImmutablePureComponent {
{!suspended && (
<div className='account__header__extra__links'>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber
value={account.get('statuses_count')}
renderer={counterRenderer('statuses')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<ShortNumber
value={account.get('following_count')}
renderer={counterRenderer('following')}
/>
</NavLink>
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<ShortNumber
value={account.get('followers_count')}
renderer={counterRenderer('followers')}

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