Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mentioned users by status view #28

Merged
merged 3 commits into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions app/controllers/api/v1/statuses/mentioned_accounts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

class Api::V1::Statuses::MentionedAccountsController < Api::BaseController
include Authorization

before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_status
after_action :insert_pagination_headers

def index
cache_if_unauthenticated!
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end

private

def load_accounts
scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_mentioned_users).to_a
end

def default_accounts
Account
.without_suspended
.includes(:mentions, :account_stat)
.references(:mentions)
.where(mentions: { status_id: @status.id })
end

def paginated_mentioned_users
Mention.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id],
params[:since_id]
)
end

def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end

def next_path
api_v1_status_mentioned_by_index_url pagination_params(max_id: pagination_max_id) if records_continue?
end

def prev_path
api_v1_status_mentioned_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end

def pagination_max_id
@accounts.last.mentions.last.id
end

def pagination_since_id
@accounts.first.mentions.first.id
end

def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end

def set_status
@status = Status.find(params[:status_id])
authorize @status, :show_mentioned_users?
rescue Mastodon::NotPermittedError
not_found
end

def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
end
end
90 changes: 90 additions & 0 deletions app/javascript/mastodon/actions/interactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';

export const MENTIONED_USERS_FETCH_REQUEST = 'MENTIONED_USERS_FETCH_REQUEST';
export const MENTIONED_USERS_FETCH_SUCCESS = 'MENTIONED_USERS_FETCH_SUCCESS';
export const MENTIONED_USERS_FETCH_FAIL = 'MENTIONED_USERS_FETCH_FAIL';

export const MENTIONED_USERS_EXPAND_REQUEST = 'MENTIONED_USERS_EXPAND_REQUEST';
export const MENTIONED_USERS_EXPAND_SUCCESS = 'MENTIONED_USERS_EXPAND_SUCCESS';
export const MENTIONED_USERS_EXPAND_FAIL = 'MENTIONED_USERS_EXPAND_FAIL';

export function reblog(status, visibility) {
return function (dispatch, getState) {
dispatch(reblogRequest(status));
Expand Down Expand Up @@ -735,3 +743,85 @@ export function unpinFail(status, error) {
skipLoading: true,
};
}

export function fetchMentionedUsers(id) {
return (dispatch, getState) => {
dispatch(fetchMentionedUsersRequest(id));

api(getState).get(`/api/v1/statuses/${id}/mentioned_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchMentionedUsersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
dispatch(fetchMentionedUsersFail(id, error));
});
};
}

export function fetchMentionedUsersRequest(id) {
return {
type: MENTIONED_USERS_FETCH_REQUEST,
id,
};
}

export function fetchMentionedUsersSuccess(id, accounts, next) {
return {
type: MENTIONED_USERS_FETCH_SUCCESS,
id,
accounts,
next,
};
}

export function fetchMentionedUsersFail(id, error) {
return {
type: MENTIONED_USERS_FETCH_FAIL,
id,
error,
};
}

export function expandMentionedUsers(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'mentioned_users', id, 'next']);
if (url === null) {
return;
}

dispatch(expandMentionedUsersRequest(id));

api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');

dispatch(importFetchedAccounts(response.data));
dispatch(expandMentionedUsersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandMentionedUsersFail(id, error)));
};
}

export function expandMentionedUsersRequest(id) {
return {
type: MENTIONED_USERS_EXPAND_REQUEST,
id,
};
}

export function expandMentionedUsersSuccess(id, accounts, next) {
return {
type: MENTIONED_USERS_EXPAND_SUCCESS,
id,
accounts,
next,
};
}

export function expandMentionedUsersFail(id, error) {
return {
type: MENTIONED_USERS_EXPAND_FAIL,
id,
error,
};
}
11 changes: 10 additions & 1 deletion app/javascript/mastodon/components/status_action_bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const messages = defineMessages({
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mentions: { id: 'status.mentions', defaultMessage: 'Mentioned users' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
Expand Down Expand Up @@ -249,6 +250,10 @@ class StatusActionBar extends ImmutablePureComponent {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`);
};

handleOpenMentions = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}/mentioned_users`);
};

handleEmbed = () => {
this.props.onEmbed(this.props.status);
};
Expand Down Expand Up @@ -315,7 +320,11 @@ class StatusActionBar extends ImmutablePureComponent {
}

if (signedIn) {
if (!simpleTimelineMenu) {
if (writtenByMe) {
menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
}

if (!simpleTimelineMenu || writtenByMe) {
menu.push(null);
}

Expand Down
90 changes: 90 additions & 0 deletions app/javascript/mastodon/features/mentioned_users/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import PropTypes from 'prop-types';

import { injectIntl, FormattedMessage } from 'react-intl';

import { Helmet } from 'react-helmet';

import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';

import { debounce } from 'lodash';

import { fetchMentionedUsers, expandMentionedUsers } from 'mastodon/actions/interactions';
import ColumnHeader from 'mastodon/components/column_header';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollableList from 'mastodon/components/scrollable_list';
import AccountContainer from 'mastodon/containers/account_container';
import Column from 'mastodon/features/ui/components/column';

const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'mentioned_users', props.params.statusId, 'items']),
hasMore: !!state.getIn(['user_lists', 'mentioned_users', props.params.statusId, 'next']),
isLoading: state.getIn(['user_lists', 'mentioned_users', props.params.statusId, 'isLoading'], true),
});

class MentionedUsers extends ImmutablePureComponent {

static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
};

UNSAFE_componentWillMount () {
if (!this.props.accountIds) {
this.props.dispatch(fetchMentionedUsers(this.props.params.statusId));
}
}

handleLoadMore = debounce(() => {
this.props.dispatch(expandMentionedUsers(this.props.params.statusId));
}, 300, { leading: true });

render () {
const { accountIds, hasMore, isLoading, multiColumn } = this.props;

if (!accountIds) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}

const emptyMessage = <FormattedMessage id='empty_column.mentioned_users' defaultMessage='No one has been mentioned by this post.' />;

return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
showBackButton
multiColumn={multiColumn}
/>

<ScrollableList
scrollKey='mentioned_users'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>

<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}

}

export default connect(mapStateToProps)(injectIntl(MentionedUsers));
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const messages = defineMessages({
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mentions: { id: 'status.mentions', defaultMessage: 'Mentioned users' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
Expand Down Expand Up @@ -95,6 +96,10 @@ class ActionBar extends PureComponent {
intl: PropTypes.object.isRequired,
};

handleOpenMentions = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}/mentioned_users`);
};

handleReplyClick = () => {
this.props.onReply(this.props.status);
};
Expand Down Expand Up @@ -264,6 +269,7 @@ class ActionBar extends PureComponent {
menu.push(null);
}

menu.push({ text: intl.formatMessage(messages.mentions), action: this.handleOpenMentions });
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
Expand Down
2 changes: 2 additions & 0 deletions app/javascript/mastodon/features/ui/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
Favourites,
EmojiReactions,
StatusReferences,
MentionedUsers,
DirectTimeline,
HashtagTimeline,
AntennaTimeline,
Expand Down Expand Up @@ -243,6 +244,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/@:acct/:statusId/favourites' component={Favourites} content={children} />
<WrappedRoute path='/@:acct/:statusId/emoji_reactions' component={EmojiReactions} content={children} />
<WrappedRoute path='/@:acct/:statusId/references' component={StatusReferences} content={children} />
<WrappedRoute path='/@:acct/:statusId/mentioned_users' component={MentionedUsers} content={children} />

{/* Legacy routes, cannot be easily factored with other routes because they share a param name */}
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
Expand Down
4 changes: 4 additions & 0 deletions app/javascript/mastodon/features/ui/util/async-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ export function StatusReferences () {
return import(/* webpackChunkName: "features/status_references" */'../../status_references');
}

export function MentionedUsers () {
return import(/* webpackChunkName: "features/mentioned_users" */'../../mentioned_users');
}

export function FollowRequests () {
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
}
Expand Down
1 change: 1 addition & 0 deletions app/javascript/mastodon/reducers/circles.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const initialState = ImmutableList();
const initialStatusesState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
loaded: true,
next: null,
});

Expand Down
17 changes: 17 additions & 0 deletions app/javascript/mastodon/reducers/user_lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ import {
EMOJI_REACTIONS_EXPAND_SUCCESS,
EMOJI_REACTIONS_EXPAND_FAIL,
STATUS_REFERENCES_FETCH_SUCCESS,
MENTIONED_USERS_FETCH_REQUEST,
MENTIONED_USERS_FETCH_SUCCESS,
MENTIONED_USERS_FETCH_FAIL,
MENTIONED_USERS_EXPAND_REQUEST,
MENTIONED_USERS_EXPAND_SUCCESS,
MENTIONED_USERS_EXPAND_FAIL,
} from '../actions/interactions';
import {
MUTES_FETCH_REQUEST,
Expand Down Expand Up @@ -92,6 +98,7 @@ const initialState = ImmutableMap({
favourited_by: initialListState,
emoji_reactioned_by: initialListState,
referred_by: initialListState,
mentioned_users: initialListState,
follow_requests: initialListState,
blocks: initialListState,
mutes: initialListState,
Expand Down Expand Up @@ -205,6 +212,16 @@ export default function userLists(state = initialState, action) {
return appendToEmojiReactionList(state, ['emoji_reactioned_by', action.id], action.accounts, action.next);
case STATUS_REFERENCES_FETCH_SUCCESS:
return state.setIn(['referred_by', action.id], ImmutableList(action.statuses.map(item => item.id)));
case MENTIONED_USERS_FETCH_SUCCESS:
return normalizeList(state, ['mentioned_users', action.id], action.accounts, action.next);
case MENTIONED_USERS_EXPAND_SUCCESS:
return appendToList(state, ['mentioned_users', action.id], action.accounts, action.next);
case MENTIONED_USERS_FETCH_REQUEST:
case MENTIONED_USERS_EXPAND_REQUEST:
return state.setIn(['mentioned_users', action.id, 'isLoading'], true);
case MENTIONED_USERS_FETCH_FAIL:
case MENTIONED_USERS_EXPAND_FAIL:
return state.setIn(['mentioned_users', action.id, 'isLoading'], false);
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:
Expand Down
Loading