Add header and emoji to list of reactions to posts

This commit is contained in:
noellabo 2021-09-17 15:20:32 +09:00
parent 7af54f7d79
commit 56fa09dbd8
25 changed files with 677 additions and 116 deletions

View file

@ -1,31 +1,28 @@
# frozen_string_literal: true
class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseController
class Api::V1::Statuses::EmojiReactionedByController < Api::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action -> { authorize_if_got_token! :read, :'read:favourites' }
before_action :set_status
after_action :insert_pagination_headers
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
@emoji_reactions = load_emoji_reactions
render json: @emoji_reactions, each_serializer: REST::EmojiReactionSerializer
end
private
def load_accounts
scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
def load_emoji_reactions
scope = default_emoji_reactions
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_emoji_reactions).to_a
end
def default_accounts
Account
.without_suspended
.includes(:emoji_reactions, :account_stat)
.references(:emoji_reactions)
.where(emoji_reactions: { status_id: @status.id })
def default_emoji_reactions
EmojiReaction
.where(status_id: @status.id)
end
def paginated_emoji_reactions
@ -47,21 +44,21 @@ class Api::V1::Statuses::EmojiReactionedByAccountsController < Api::BaseControll
end
def prev_path
unless @accounts.empty?
unless @emoji_reactions.empty?
api_v1_status_emoji_reactioned_by_index_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.emoji_reactions.last.id
@emoji_reactions.last.id
end
def pagination_since_id
@accounts.first.emoji_reactions.first.id
@emoji_reactions.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
@emoji_reactions.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def set_status

View file

@ -1,5 +1,6 @@
import api from '../api';
import api, { getLinks } from '../api';
import { importFetchedAccounts, importFetchedStatus } from './importer';
import { fetchRelationships } from './accounts';
import { me } from '../initial_state';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
@ -22,18 +23,34 @@ export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
export const EMOJI_REACTIONS_FETCH_REQUEST = 'EMOJI_REACTIONS_FETCH_REQUEST';
export const EMOJI_REACTIONS_FETCH_SUCCESS = 'EMOJI_REACTIONS_FETCH_SUCCESS';
export const EMOJI_REACTIONS_FETCH_FAIL = 'EMOJI_REACTIONS_FETCH_FAIL';
export const EMOJI_REACTIONS_EXPAND_REQUEST = 'EMOJI_REACTIONS_EXPAND_REQUEST';
export const EMOJI_REACTIONS_EXPAND_SUCCESS = 'EMOJI_REACTIONS_EXPAND_SUCCESS';
export const EMOJI_REACTIONS_EXPAND_FAIL = 'EMOJI_REACTIONS_EXPAND_FAIL';
export const MENTIONS_FETCH_REQUEST = 'MENTIONS_FETCH_REQUEST';
export const MENTIONS_FETCH_SUCCESS = 'MENTIONS_FETCH_SUCCESS';
export const MENTIONS_FETCH_FAIL = 'MENTIONS_FETCH_FAIL';
export const MENTIONS_EXPAND_REQUEST = 'MENTIONS_EXPAND_REQUEST';
export const MENTIONS_EXPAND_SUCCESS = 'MENTIONS_EXPAND_SUCCESS';
export const MENTIONS_EXPAND_FAIL = 'MENTIONS_EXPAND_FAIL';
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
@ -291,8 +308,10 @@ export function fetchReblogs(id) {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchReblogsSuccess(id, response.data));
dispatch(fetchRelationships(response.data.map(accounts => accounts.id)));
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchReblogsFail(id, error));
});
@ -306,11 +325,12 @@ export function fetchReblogsRequest(id) {
};
};
export function fetchReblogsSuccess(id, accounts) {
export function fetchReblogsSuccess(id, accounts, next) {
return {
type: REBLOGS_FETCH_SUCCESS,
id,
accounts,
next,
};
};
@ -321,13 +341,59 @@ export function fetchReblogsFail(id, error) {
};
};
export function expandReblogs(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next'], null);
if (url === null || getState().getIn(['user_lists', 'reblogged_by', id, 'isLoading'])) {
return;
}
dispatch(expandReblogsRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchRelationships(response.data.map(accounts => accounts.id)));
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandReblogsFail(id, error));
});
};
};
export function expandReblogsRequest(id) {
return {
type: REBLOGS_EXPAND_REQUEST,
id,
};
};
export function expandReblogsSuccess(id, accounts, next) {
return {
type: REBLOGS_EXPAND_SUCCESS,
id,
accounts,
next,
};
};
export function expandReblogsFail(id, error) {
return {
type: REBLOGS_EXPAND_FAIL,
error,
};
};
export function fetchFavourites(id) {
return (dispatch, getState) => {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFavouritesSuccess(id, response.data));
dispatch(fetchRelationships(response.data.map(accounts => accounts.id)));
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
});
@ -341,11 +407,12 @@ export function fetchFavouritesRequest(id) {
};
};
export function fetchFavouritesSuccess(id, accounts) {
export function fetchFavouritesSuccess(id, accounts, next) {
return {
type: FAVOURITES_FETCH_SUCCESS,
id,
accounts,
next,
};
};
@ -356,13 +423,59 @@ export function fetchFavouritesFail(id, error) {
};
};
export function expandFavourites(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'favourited_by', id, 'next'], null);
if (url === null || getState().getIn(['user_lists', 'favourited_by', id, 'isLoading'])) {
return;
}
dispatch(expandFavouritesRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchRelationships(response.data.map(accounts => accounts.id)));
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritesFail(id, error));
});
};
};
export function expandFavouritesRequest(id) {
return {
type: FAVOURITES_EXPAND_REQUEST,
id,
};
};
export function expandFavouritesSuccess(id, accounts, next) {
return {
type: FAVOURITES_EXPAND_SUCCESS,
id,
accounts,
next,
};
};
export function expandFavouritesFail(id, error) {
return {
type: FAVOURITES_EXPAND_FAIL,
error,
};
};
export function fetchEmojiReactions(id) {
return (dispatch, getState) => {
dispatch(fetchEmojiReactionsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/emoji_reactioned_by`).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchEmojiReactionsSuccess(id, response.data));
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(emojiReaction => emojiReaction.account)));
dispatch(fetchRelationships(response.data.map(emojiReaction => emojiReaction.account.id)));
dispatch(fetchEmojiReactionsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchEmojiReactionsFail(id, error));
});
@ -376,11 +489,12 @@ export function fetchEmojiReactionsRequest(id) {
};
};
export function fetchEmojiReactionsSuccess(id, accounts) {
export function fetchEmojiReactionsSuccess(id, emojiReactions, next) {
return {
type: EMOJI_REACTIONS_FETCH_SUCCESS,
id,
accounts,
emojiReactions,
next,
};
};
@ -391,13 +505,59 @@ export function fetchEmojiReactionsFail(id, error) {
};
};
export function expandEmojiReactions(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next'], null);
if (url === null || getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'isLoading'])) {
return;
}
dispatch(expandEmojiReactionsRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(emojiReaction => emojiReaction.account)));
dispatch(fetchRelationships(response.data.map(emojiReaction => emojiReaction.account.id)));
dispatch(expandEmojiReactionsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandEmojiReactionsFail(id, error));
});
};
};
export function expandEmojiReactionsRequest(id) {
return {
type: EMOJI_REACTIONS_EXPAND_REQUEST,
id,
};
};
export function expandEmojiReactionsSuccess(id, emojiReactions, next) {
return {
type: EMOJI_REACTIONS_EXPAND_SUCCESS,
id,
emojiReactions,
next,
};
};
export function expandEmojiReactionsFail(id, error) {
return {
type: EMOJI_REACTIONS_EXPAND_FAIL,
error,
};
};
export function fetchMentions(id) {
return (dispatch, getState) => {
dispatch(fetchMentionsRequest(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(fetchMentionsSuccess(id, response.data));
dispatch(fetchRelationships(response.data.map(accounts => accounts.id)));
dispatch(fetchMentionsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchMentionsFail(id, error));
});
@ -411,11 +571,12 @@ export function fetchMentionsRequest(id) {
};
};
export function fetchMentionsSuccess(id, accounts) {
export function fetchMentionsSuccess(id, accounts, next) {
return {
type: MENTIONS_FETCH_SUCCESS,
id,
accounts,
next,
};
};
@ -426,6 +587,50 @@ export function fetchMentionsFail(id, error) {
};
};
export function expandMentions(id) {
return (dispatch, getState) => {
const url = getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'next'], null);
if (url === null || getState().getIn(['user_lists', 'emoji_reactioned_by', id, 'isLoading'])) {
return;
}
dispatch(expandMentionsRequest(id));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
dispatch(fetchRelationships(response.data.map(accounts => accounts.id)));
dispatch(expandMentionsSuccess(id, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandMentionsFail(id, error));
});
};
};
export function expandMentionsRequest(id) {
return {
type: MENTIONS_EXPAND_REQUEST,
id,
};
};
export function expandMentionsSuccess(id, accounts, next) {
return {
type: MENTIONS_EXPAND_SUCCESS,
id,
accounts,
next,
};
};
export function expandMentionsFail(id, error) {
return {
type: MENTIONS_EXPAND_FAIL,
error,
};
};
export function pin(status) {
return (dispatch, getState) => {
dispatch(pinRequest(status));

View file

@ -47,7 +47,7 @@ export function fetchSuggestionsFail(error) {
};
};
export const dismissSuggestion = accountId => (dispatch, getState) => {
export const dismissSuggestion = (accountId, withRelationships = false) => (dispatch, getState) => {
dispatch({
type: SUGGESTIONS_DISMISS,
id: accountId,
@ -59,6 +59,10 @@ export const dismissSuggestion = accountId => (dispatch, getState) => {
api(getState).get('/api/v2/suggestions').then(response => {
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchSuggestionsSuccess(response.data));
if (withRelationships) {
dispatch(fetchRelationships(response.data.map(item => item.account.id)));
}
}).catch(error => dispatch(fetchSuggestionsFail(error)));
}).catch(() => {});
};

View file

@ -38,6 +38,7 @@ class Account extends ImmutablePureComponent {
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
onActionClick: PropTypes.func,
append: PropTypes.node,
};
handleFollow = (e) => {
@ -77,7 +78,7 @@ class Account extends ImmutablePureComponent {
}
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, append } = this.props;
if (!account) {
return <div />;
@ -175,6 +176,8 @@ class Account extends ImmutablePureComponent {
<div className='account__relationship'>
{buttons}
</div>
{append}
</div>
</div>
);

View file

@ -34,7 +34,9 @@ const messages = defineMessages({
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
emoji_reaction: { id: 'status.emoji_reaction', defaultMessage: 'Emoji reaction' },
open: { id: 'status.open', defaultMessage: 'Expand this status' },
open_emoji_reactions: { id: 'status.open_emoji_reactions', defaultMessage: 'Open emoji reactions list to this post' },
show_reblogs: { id: 'status.show_reblogs', defaultMessage: 'Show boosted users' },
show_favourites: { id: 'status.show_favourites', defaultMessage: 'Show favourited users' },
show_emoji_reactions: { id: 'status.show_emoji_reactions', defaultMessage: 'Show emoji reactioned users' },
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
@ -225,10 +227,6 @@ class StatusActionBar extends ImmutablePureComponent {
this.props.onEmbed(this.props.status);
}
handleEmojiReactions = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/emoji_reactions`);
}
handleReport = () => {
this.props.onReport(this.props.status);
}
@ -256,6 +254,18 @@ class StatusActionBar extends ImmutablePureComponent {
}
}
handleReblogs = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/reblogs`);
}
handleFavourites = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/favourites`);
}
handleEmojiReactions = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/emoji_reactions`);
}
handleEmojiPick = data => {
const { addEmojiReaction, status } = this.props;
addEmojiReaction(status, data.native.replace(/:/g, ''), null, null, null);
@ -275,6 +285,13 @@ class StatusActionBar extends ImmutablePureComponent {
const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me;
const limitedByMe = status.get('visibility') === 'limited' && status.get('circle_id');
const reblogged = status.get('reblogged');
const favourited = status.get('favourited');
const bookmarked = status.get('bookmarked');
const emoji_reactioned = status.get('emoji_reactioned');
const reblogsCount = status.get('reblogs_count');
const favouritesCount = status.get('favourites_count');
const [ _, domain ] = account.get('acct').split('@');
let menu = [];
@ -285,19 +302,38 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
menu.push({ text: intl.formatMessage(messages.open_emoji_reactions), action: this.handleEmojiReactions });
if (reblogsCount > 0 || favouritesCount > 0 || !status.get('emoji_reactions').isEmpty()) {
menu.push(null);
}
if (reblogsCount > 0) {
menu.push({ text: intl.formatMessage(messages.show_reblogs), action: this.handleReblogs });
}
if (favouritesCount > 0) {
menu.push({ text: intl.formatMessage(messages.show_favourites), action: this.handleFavourites });
}
if (!status.get('emoji_reactions').isEmpty()) {
menu.push({ text: intl.formatMessage(messages.show_emoji_reactions), action: this.handleEmojiReactions });
}
if (domain) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.openDomainTimeline, { domain }), action: this.handleOpenDomainTimeline });
}
menu.push(null);
if (!show_bookmark_button) {
menu.push({ text: intl.formatMessage(bookmarked ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
}
if (writtenByMe && publicStatus && !expired) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
if (!show_bookmark_button || writtenByMe && publicStatus && !expired) {
if (!show_bookmark_button) {
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
}
if (writtenByMe && publicStatus && !expired) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push(null);
}
@ -333,9 +369,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
if (domain) {
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
@ -343,7 +377,6 @@ class StatusActionBar extends ImmutablePureComponent {
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
menu.push({ text: intl.formatMessage(messages.openDomainTimeline, { domain }), action: this.handleOpenDomainTimeline });
}
if (isStaff) {
@ -366,7 +399,7 @@ class StatusActionBar extends ImmutablePureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = '';
if (status.get('reblogged')) {
if (reblogged) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
@ -383,18 +416,18 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<div className='status__action-bar'>
<IconButton className='status__action-bar-button' disabled={expired} title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate disabled={!status.get('favourited') && expired} active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={reblogged} pressed={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate disabled={!favourited && expired} active={favourited} pressed={favourited} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{show_quote_button && <IconButton className='status__action-bar-button' disabled={anonymousAccess || !publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} />}
{shareButton}
{show_bookmark_button && <IconButton className='status__action-bar-button bookmark-icon' disabled={!status.get('bookmarked') && expired} active={status.get('bookmarked')} pressed={status.get('bookmarked')} title={intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />}
{show_bookmark_button && <IconButton className='status__action-bar-button bookmark-icon' disabled={!bookmarked && expired} active={bookmarked} pressed={bookmarked} title={intl.formatMessage(bookmarked ? messages.removeBookmark : messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />}
{enableReaction && <div className='status__action-bar-dropdown'>
<ReactionPickerDropdownContainer
scrollKey={scrollKey}
disabled={expired}
active={status.get('emoji_reactioned')}
pressed={status.get('emoji_reactioned')}
active={emoji_reactioned}
pressed={emoji_reactioned}
className='status__action-bar-button'
disabled={anonymousAccess}
status={status}

View file

@ -10,9 +10,9 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
fetchSuggestions: () => dispatch(fetchSuggestions()),
fetchSuggestions: () => dispatch(fetchSuggestions(true)),
expandSearch: type => dispatch(expandSearch(type)),
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'), true)),
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);

View file

@ -4,22 +4,58 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchEmojiReactions } from '../../actions/interactions';
import { fetchEmojiReactions, expandEmojiReactions } from '../../actions/interactions';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
import Emoji from '../../components/emoji';
import { createSelector } from 'reselect';
import { Map as ImmutableMap } from 'immutable';
import ReactedHeaderContaier from '../reactioned/containers/header_container';
import { debounce } from 'lodash';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap()));
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId]),
emojiReactions: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'items']),
isLoading: state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'isLoading'], true),
hasMore: !!state.getIn(['user_lists', 'emoji_reactioned_by', props.params.statusId, 'next']),
emojiMap: customEmojiMap(state),
});
class Reaction extends ImmutablePureComponent {
static propTypes = {
emojiReaction: ImmutablePropTypes.map.isRequired,
emojiMap: ImmutablePropTypes.map.isRequired,
};
state = {
hovered: false,
};
handleMouseEnter = () => this.setState({ hovered: true })
handleMouseLeave = () => this.setState({ hovered: false })
render () {
const { emojiReaction, emojiMap } = this.props;
return (
<div className='account__emoji_reaction' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<Emoji hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} />
</div>
);
};
}
export default @connect(mapStateToProps)
@injectIntl
class EmojiReactions extends ImmutablePureComponent {
@ -27,13 +63,16 @@ class EmojiReactions extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.list,
emojiReactions: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
emojiMap: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
componentWillMount () {
if (!this.props.accountIds) {
if (!this.props.emojiReactions) {
this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
}
}
@ -48,10 +87,14 @@ class EmojiReactions extends ImmutablePureComponent {
this.props.dispatch(fetchEmojiReactions(this.props.params.statusId));
}
render () {
const { intl, accountIds, multiColumn } = this.props;
handleLoadMore = debounce(() => {
this.props.dispatch(expandEmojiReactions(this.props.params.statusId));
}, 300, { leading: true })
if (!accountIds) {
render () {
const { intl, emojiReactions, multiColumn, emojiMap, hasMore, isLoading } = this.props;
if (!emojiReactions) {
return (
<Column>
<LoadingIndicator />
@ -71,13 +114,18 @@ class EmojiReactions extends ImmutablePureComponent {
)}
/>
<ReactedHeaderContaier statusId={this.props.params.statusId} />
<ScrollableList
scrollKey='emoji_reactions'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
{emojiReactions.map(emojiReaction =>
<AccountContainer key={emojiReaction.get('account')+emojiReaction.get('name')} id={emojiReaction.get('account')} withNote={false} append={<Reaction emojiReaction={emojiReaction} emojiMap={emojiMap} />} />,
)}
</ScrollableList>
</Column>

View file

@ -4,20 +4,24 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions';
import { fetchFavourites, expandFavourites } from '../../actions/interactions';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
import ReactedHeaderContaier from '../reactioned/containers/header_container';
import { debounce } from 'lodash';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
});
export default @connect(mapStateToProps)
@ -30,6 +34,8 @@ class Favourites extends ImmutablePureComponent {
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
componentWillMount () {
@ -48,8 +54,12 @@ class Favourites extends ImmutablePureComponent {
this.props.dispatch(fetchFavourites(this.props.params.statusId));
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFavourites(this.props.params.statusId));
}, 300, { leading: true })
render () {
const { intl, accountIds, multiColumn } = this.props;
const { intl, accountIds, multiColumn, hasMore, isLoading } = this.props;
if (!accountIds) {
return (
@ -71,8 +81,13 @@ class Favourites extends ImmutablePureComponent {
)}
/>
<ReactedHeaderContaier statusId={this.props.params.statusId} />
<ScrollableList
scrollKey='favourites'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

View file

@ -4,15 +4,18 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchMentions } from '../../actions/interactions';
import { fetchMentions, expandMentions } from '../../actions/interactions';
import { injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import ColumnHeader from '../../components/column_header';
import { debounce } from 'lodash';
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'mentioned_by', props.params.statusId]),
accountIds: state.getIn(['user_lists', 'mentioned_by', props.params.statusId, 'items']),
isLoading: state.getIn(['user_lists', 'mentioned_by', props.params.statusId, 'isLoading'], true),
hasMore: !!state.getIn(['user_lists', 'mentioned_by', props.params.statusId, 'next']),
});
export default @connect(mapStateToProps)
@ -25,6 +28,8 @@ class Mentions extends ImmutablePureComponent {
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
componentWillMount () {
@ -39,8 +44,12 @@ class Mentions extends ImmutablePureComponent {
}
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandMentions(this.props.params.statusId));
}, 300, { leading: true })
render () {
const { accountIds, multiColumn } = this.props;
const { accountIds, multiColumn, hasMore, isLoading } = this.props;
if (!accountIds) {
return (
@ -61,6 +70,9 @@ class Mentions extends ImmutablePureComponent {
<ScrollableList
scrollKey='mentions'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

View file

@ -0,0 +1,51 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
import Icon from '../../../components/icon';
export default class Header extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
hideTabs: PropTypes.bool,
};
handleStatusBackClick = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}`);
};
render () {
const { status, hideTabs } = this.props;
if (status === null) {
return null;
}
return (
<div className='status-reactioned__header'>
<div className='status-reactioned__back-to-post-button'>
<button onClick={this.handleStatusBackClick} className='column-header__back-button'>
<Icon id='chevron-left' className='reactioned-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button_to_pots.label' defaultMessage='Back to post detail' />
</button>
</div>
{!hideTabs && (
<div className='status-reactioned__section-headline'>
<NavLink exact to={`/statuses/${status.get('id')}/reblogs`}><FormattedMessage id='status.reblog' defaultMessage='Boost' /></NavLink>
<NavLink exact to={`/statuses/${status.get('id')}/favourites`}><FormattedMessage id='status.favourite' defaultMessage='Favourite' /></NavLink>
<NavLink exact to={`/statuses/${status.get('id')}/emoji_reactions`}><FormattedMessage id='status.emoji' defaultMessage='Emoji' /></NavLink>
</div>
)}
</div>
);
}
}

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { makeGetStatus } from '../../../selectors';
import Header from '../components/header';
import { injectIntl } from 'react-intl';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, { statusId }) => ({
status: getStatus(state, { id: statusId }),
});
return mapStateToProps;
};
export default injectIntl(connect(makeMapStateToProps)(Header));

View file

@ -4,20 +4,24 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions';
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column';
import ScrollableList from '../../components/scrollable_list';
import Icon from 'mastodon/components/icon';
import ColumnHeader from '../../components/column_header';
import ReactedHeaderContaier from '../reactioned/containers/header_container';
import { debounce } from 'lodash';
const messages = defineMessages({
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
});
const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'items']),
isLoading: state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'isLoading'], true),
hasMore: !!state.getIn(['user_lists', 'reblogged_by', props.params.statusId, 'next']),
});
export default @connect(mapStateToProps)
@ -30,6 +34,8 @@ class Reblogs extends ImmutablePureComponent {
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
};
componentWillMount () {
@ -48,8 +54,12 @@ class Reblogs extends ImmutablePureComponent {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandReblogs(this.props.params.statusId));
}, 300, { leading: true })
render () {
const { intl, accountIds, multiColumn } = this.props;
const { intl, accountIds, multiColumn, hasMore, isLoading } = this.props;
if (!accountIds) {
return (
@ -71,8 +81,13 @@ class Reblogs extends ImmutablePureComponent {
)}
/>
<ReactedHeaderContaier statusId={this.props.params.statusId} />
<ScrollableList
scrollKey='reblogs'
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

View file

@ -25,6 +25,9 @@ const messages = defineMessages({
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
emoji_reaction: { id: 'status.emoji_reaction', defaultMessage: 'Emoji reaction' },
show_reblogs: { id: 'status.show_reblogs', defaultMessage: 'Show boosted users' },
show_favourites: { id: 'status.show_favourites', defaultMessage: 'Show favourited users' },
show_emoji_reactions: { id: 'status.show_emoji_reactions', defaultMessage: 'Show emoji reactioned users' },
more: { id: 'status.more', defaultMessage: 'More' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
@ -209,6 +212,18 @@ class ActionBar extends React.PureComponent {
}
}
handleReblogs = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/reblogs`);
}
handleFavourites = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/favourites`);
}
handleEmojiReactions = () => {
this.context.router.history.push(`/statuses/${this.props.status.get('id')}/emoji_reactions`);
}
handleEmojiPick = data => {
const { addEmojiReaction, status } = this.props;
addEmojiReaction(status, data.native.replace(/:/g, ''), null, null, null);
@ -227,6 +242,13 @@ class ActionBar extends React.PureComponent {
const account = status.get('account');
const writtenByMe = status.getIn(['account', 'id']) === me;
const limitedByMe = status.get('visibility') === 'limited' && status.get('circle_id');
const reblogged = status.get('reblogged');
const favourited = status.get('favourited');
const bookmarked = status.get('bookmarked');
const emoji_reactioned = status.get('emoji_reactioned');
const reblogsCount = status.get('reblogs_count');
const favouritesCount = status.get('favourites_count');
const [ _, domain ] = account.get('acct').split('@');
const expires_at = status.get('expires_at')
const expires_date = expires_at && new Date(expires_at)
@ -237,9 +259,31 @@ class ActionBar extends React.PureComponent {
if (publicStatus && !expired) {
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
if (reblogsCount > 0 || favouritesCount > 0 || !status.get('emoji_reactions').isEmpty()) {
menu.push(null);
}
if (reblogsCount > 0) {
menu.push({ text: intl.formatMessage(messages.show_reblogs), action: this.handleReblogs });
}
if (favouritesCount > 0) {
menu.push({ text: intl.formatMessage(messages.show_favourites), action: this.handleFavourites });
}
if (!status.get('emoji_reactions').isEmpty()) {
menu.push({ text: intl.formatMessage(messages.show_emoji_reactions), action: this.handleEmojiReactions });
}
if (domain) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.openDomainTimeline, { domain }), action: this.handleOpenDomainTimeline });
}
menu.push(null);
if (writtenByMe) {
if (publicStatus && !expired) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
@ -248,9 +292,9 @@ class ActionBar extends React.PureComponent {
if (limitedByMe) {
menu.push({ text: intl.formatMessage(messages.showMemberList), action: this.handleMemberListClick });
menu.push(null);
}
menu.push(null);
if (!expired) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
@ -276,9 +320,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
if (domain) {
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
@ -286,7 +328,6 @@ class ActionBar extends React.PureComponent {
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain });
}
menu.push({ text: intl.formatMessage(messages.openDomainTimeline, { domain }), action: this.handleOpenDomainTimeline });
}
if (isStaff) {
@ -310,7 +351,7 @@ class ActionBar extends React.PureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle;
if (status.get('reblogged')) {
if (reblogged) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
@ -323,17 +364,17 @@ class ActionBar extends React.PureComponent {
return (
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton disabled={expired} title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} disabled={!status.get('favourited') && expired} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate || expired} active={reblogged} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={favourited} disabled={!favourited && expired} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{show_quote_button && <div className='detailed-status__button'><IconButton disabled={!publicStatus || expired} title={!publicStatus ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-right' onClick={this.handleQuoteClick} /></div>}
{shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} disabled={!status.get('bookmarked') && expired} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={bookmarked} disabled={!bookmarked && expired} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
{enableReaction && <div className='detailed-status__action-bar-dropdown'>
<ReactionPickerDropdownContainer
disabled={expired}
active={status.get('emoji_reactioned')}
pressed={status.get('emoji_reactioned')}
active={emoji_reactioned}
pressed={emoji_reactioned}
className='status__action-bar-button'
status={status}
title={intl.formatMessage(messages.emoji_reaction)}

View file

@ -43,13 +43,13 @@ class Suggestions extends ImmutablePureComponent {
fetchSuggestions = () => {
const { dispatch } = this.props;
dispatch(fetchSuggestions());
dispatch(fetchSuggestions(true));
}
dismissSuggestion = account => {
const { dispatch } = this.props;
dispatch(dismissSuggestion(account.get('id')));
dispatch(dismissSuggestion(account.get('id'), true));
}
handlePin = () => {

View file

@ -103,6 +103,7 @@
"column.pins": "Pinned posts",
"column.public": "Federated timeline",
"column_back_button.label": "Back",
"column_back_button_to_pots.label": "Back to post detail",
"column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left",
"column_header.moveRight_settings": "Move column to the right",
@ -495,6 +496,7 @@
"status.detailed_status": "Detailed conversation view",
"status.direct": "Direct message @{name}",
"status.embed": "Embed",
"status.emoji": "Emoji",
"status.emoji_reaction": "Emoji reaction",
"status.favourite": "Favourite",
"status.filtered": "Filtered",
@ -506,7 +508,6 @@
"status.mute_conversation": "Mute conversation",
"status.muted_quote": "Muted quote",
"status.open": "Expand this post",
"status.open_emoji_reactions": "Open emoji reactions list to this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.quote": "Quote",
@ -522,12 +523,15 @@
"status.report": "Report @{name}",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_emoji_reactions": "Show emoji reactioned users",
"status.show_favourites": "Show favourited users",
"status.show_less": "Show less",
"status.show_less_all": "Show less for all",
"status.show_member_list": "Show member list",
"status.show_more": "Show more",
"status.show_more_all": "Show more for all",
"status.show_poll": "Show poll",
"status.show_reblogs": "Show boosted users",
"status.show_thread": "Show thread",
"status.uncached_media_warning": "Not available",
"status.unlisted_quote": "Unlisted quote",

View file

@ -103,6 +103,7 @@
"column.pins": "固定された投稿",
"column.public": "連合タイムライン",
"column_back_button.label": "戻る",
"column_back_button_to_pots.label": "投稿の詳細に戻る",
"column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する",
"column_header.moveRight_settings": "カラムを右に移動する",
@ -496,6 +497,7 @@
"status.detailed_status": "詳細な会話ビュー",
"status.direct": "@{name}さんにダイレクトメッセージ",
"status.embed": "埋め込み",
"status.emoji": "絵文字",
"status.emoji_reaction": "絵文字リアクション",
"status.favourite": "お気に入り",
"status.filtered": "フィルターされました",
@ -507,7 +509,6 @@
"status.mute_conversation": "会話をミュート",
"status.muted_quote": "ミュートされた引用",
"status.open": "詳細を表示",
"status.open_emoji_reactions": "この投稿への絵文字リアクション一覧",
"status.pin": "プロフィールに固定表示",
"status.pinned": "固定された投稿",
"status.quote": "引用",
@ -523,12 +524,15 @@
"status.report": "@{name}さんを通報",
"status.sensitive_warning": "閲覧注意",
"status.share": "共有",
"status.show_emoji_reactions": "絵文字リアクションしたユーザーを表示",
"status.show_favourites": "お気に入りしたユーザーを表示",
"status.show_less": "隠す",
"status.show_less_all": "全て隠す",
"status.show_member_list": "メンバーリストを表示",
"status.show_more": "もっと見る",
"status.show_more_all": "全て見る",
"status.show_poll": "アンケートを表示",
"status.show_reblogs": "ブーストしたユーザーを表示",
"status.show_thread": "スレッドを表示",
"status.uncached_media_warning": "利用できません",
"status.unlisted_quote": "未収載の引用",

View file

@ -31,9 +31,29 @@ import {
} from '../actions/accounts';
import {
REBLOGS_FETCH_SUCCESS,
REBLOGS_FETCH_REQUEST,
REBLOGS_FETCH_FAIL,
REBLOGS_EXPAND_SUCCESS,
REBLOGS_EXPAND_REQUEST,
REBLOGS_EXPAND_FAIL,
FAVOURITES_FETCH_SUCCESS,
FAVOURITES_FETCH_REQUEST,
FAVOURITES_FETCH_FAIL,
FAVOURITES_EXPAND_SUCCESS,
FAVOURITES_EXPAND_REQUEST,
FAVOURITES_EXPAND_FAIL,
EMOJI_REACTIONS_FETCH_REQUEST,
EMOJI_REACTIONS_FETCH_SUCCESS,
EMOJI_REACTIONS_FETCH_FAIL,
EMOJI_REACTIONS_EXPAND_REQUEST,
EMOJI_REACTIONS_EXPAND_SUCCESS,
EMOJI_REACTIONS_EXPAND_FAIL,
MENTIONS_FETCH_SUCCESS,
MENTIONS_FETCH_REQUEST,
MENTIONS_FETCH_FAIL,
MENTIONS_EXPAND_SUCCESS,
MENTIONS_EXPAND_REQUEST,
MENTIONS_EXPAND_FAIL,
} from '../actions/interactions';
import {
BLOCKS_FETCH_REQUEST,
@ -67,7 +87,7 @@ import {
DIRECTORY_EXPAND_SUCCESS,
DIRECTORY_EXPAND_FAIL,
} from 'mastodon/actions/directory';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const initialListState = ImmutableMap({
next: null,
@ -108,6 +128,25 @@ const normalizeFollowRequest = (state, notification) => {
});
};
const normalizeEmojiReaction = emojiReaction => {
const normalizeEmojiReaction = { ...emojiReaction, account: emojiReaction.account.id };
return fromJS(normalizeEmojiReaction);
};
const normalizeEmojiReactions = (state, path, emojiReactions, next) => {
return state.setIn(path, ImmutableMap({
next,
items: ImmutableList(emojiReactions.map(normalizeEmojiReaction)),
isLoading: false,
}));
};
const appendToEmojiReactions = (state, path, emojiReactions, next) => {
return state.updateIn(path, map => {
return map.set('next', next).set('isLoading', false).update('items', list => list.concat(emojiReactions.map(normalizeEmojiReaction)));
});
};
export default function userLists(state = initialState, action) {
switch(action.type) {
case FOLLOWERS_FETCH_SUCCESS:
@ -141,13 +180,45 @@ export default function userLists(state = initialState, action) {
case SUBSCRIBING_EXPAND_FAIL:
return state.setIn(['subscribing', action.id, 'isLoading'], false);
case REBLOGS_FETCH_SUCCESS:
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
return normalizeList(state, ['reblogged_by', action.id], action.accounts, action.next);
case REBLOGS_EXPAND_SUCCESS:
return appendToList(state, ['reblogged_by', action.id], action.accounts, action.next);
case REBLOGS_FETCH_REQUEST:
case REBLOGS_EXPAND_REQUEST:
return state.setIn(['reblogged_by', action.id, 'isLoading'], true);
case REBLOGS_FETCH_FAIL:
case REBLOGS_EXPAND_FAIL:
return state.setIn(['reblogged_by', action.id, 'isLoading'], false);
case FAVOURITES_FETCH_SUCCESS:
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
return normalizeList(state, ['favourited_by', action.id], action.accounts, action.next);
case FAVOURITES_EXPAND_SUCCESS:
return appendToList(state, ['favourited_by', action.id], action.accounts, action.next);
case FAVOURITES_FETCH_REQUEST:
case FAVOURITES_EXPAND_REQUEST:
return state.setIn(['favourited_by', action.id, 'isLoading'], true);
case FAVOURITES_FETCH_FAIL:
case FAVOURITES_EXPAND_FAIL:
return state.setIn(['favourited_by', action.id, 'isLoading'], false);
case EMOJI_REACTIONS_FETCH_SUCCESS:
return state.setIn(['emoji_reactioned_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
return normalizeEmojiReactions(state, ['emoji_reactioned_by', action.id], action.emojiReactions, action.next);
case EMOJI_REACTIONS_EXPAND_SUCCESS:
return appendToEmojiReactions(state, ['emoji_reactioned_by', action.id], action.emojiReactions, action.next);
case EMOJI_REACTIONS_FETCH_REQUEST:
case EMOJI_REACTIONS_EXPAND_REQUEST:
return state.setIn(['emoji_reactioned_by', action.id, 'isLoading'], true);
case EMOJI_REACTIONS_FETCH_FAIL:
case EMOJI_REACTIONS_EXPAND_FAIL:
return state.setIn(['emoji_reactioned_by', action.id, 'isLoading'], false);
case MENTIONS_FETCH_SUCCESS:
return state.setIn(['mentioned_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
return normalizeList(state, ['mentioned_by', action.id], action.accounts, action.next);
case MENTIONS_EXPAND_SUCCESS:
return appendToList(state, ['mentioned_by', action.id], action.accounts, action.next);
case MENTIONS_FETCH_REQUEST:
case MENTIONS_EXPAND_REQUEST:
return state.setIn(['mentioned_by', action.id, 'isLoading'], true);
case MENTIONS_FETCH_FAIL:
case MENTIONS_EXPAND_FAIL:
return state.setIn(['mentioned_by', action.id, 'isLoading'], false);
case NOTIFICATIONS_UPDATE:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS:

View file

@ -76,7 +76,8 @@ html {
.column-header__button,
.column-header__button.active,
.account__header__bar,
.directory__card__extra {
.directory__card__extra,
.status-reactioned__back-to-post-button {
background: $white;
}

View file

@ -1619,10 +1619,17 @@ a .account__avatar {
.account__relationship {
height: 18px;
padding: 10px;
padding: 9px;
white-space: nowrap;
}
.account__emoji_reaction {
.emojione {
width: 36px;
height: 36px;
}
}
.account__disclaimer {
padding: 10px;
border-top: 1px solid lighten($ui-base-color, 8%);
@ -3059,6 +3066,11 @@ a.account__display-name {
}
}
.status-reactioned__back-to-post-button {
padding: 12px 14px;
background: lighten($ui-base-color, 4%);
}
.column-header__back-button {
background: lighten($ui-base-color, 4%);
border: 0;
@ -6534,7 +6546,8 @@ a.status-card.compact:hover {
}
.notification__filter-bar,
.account__section-headline {
.account__section-headline,
.status-reactioned__section-headline {
background: darken($ui-base-color, 4%);
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;

View file

@ -20,7 +20,7 @@ class InlineRenderer
when :reaction
serializer = REST::ReactionSerializer
when :emoji_reaction
serializer = REST::EmojiReactionSerializer
serializer = REST::GroupedEmojiReactionSerializer
when :encrypted_message
serializer = REST::EncryptedMessageSerializer
else

View file

@ -311,7 +311,7 @@ class Status < ApplicationRecord
def generate_grouped_emoji_reactions
records = emoji_reactions.group(:name).order(Arel.sql('MIN(created_at) ASC')).select('name, min(custom_emoji_id) as custom_emoji_id, count(*) as count, array_agg(account_id::text order by created_at) as account_ids')
Oj.dump(ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::EmojiReactionSerializer, scope: nil, scope_name: :current_user))
Oj.dump(ActiveModelSerializers::SerializableResource.new(records, each_serializer: REST::GroupedEmojiReactionSerializer, scope: nil, scope_name: :current_user))
end
def refresh_grouped_emoji_reactions!

View file

@ -3,30 +3,18 @@
class REST::EmojiReactionSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :name, :count
attributes :name
attribute :me, if: :current_user?
attribute :url, if: :custom_emoji?
attribute :static_url, if: :custom_emoji?
attribute :domain, if: :custom_emoji?
attribute :account_ids, if: :has_account_ids?
def count
object.respond_to?(:count) ? object.count : 0
end
def current_user?
!current_user.nil?
end
belongs_to :account, serializer: REST::AccountSerializer
def custom_emoji?
object.custom_emoji.present?
end
def has_account_ids?
object.respond_to?(:account_ids)
end
def url
full_asset_url(object.custom_emoji.image.url)
end
@ -39,4 +27,3 @@ class REST::EmojiReactionSerializer < ActiveModel::Serializer
object.custom_emoji.domain
end
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
class REST::GroupedEmojiReactionSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :name, :count
attribute :me, if: :current_user?
attribute :url, if: :custom_emoji?
attribute :static_url, if: :custom_emoji?
attribute :domain, if: :custom_emoji?
attribute :account_ids, if: :has_account_ids?
def count
object.respond_to?(:count) ? object.count : 0
end
def current_user?
!current_user.nil?
end
def custom_emoji?
object.custom_emoji.present?
end
def has_account_ids?
object.respond_to?(:account_ids)
end
def url
full_asset_url(object.custom_emoji.image.url)
end
def static_url
full_asset_url(object.custom_emoji.image.url(:static))
end
def domain
object.custom_emoji.domain
end
end

View file

@ -24,7 +24,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
object.type == :emoji_reaction
end
class EmojiReactionSerializer < REST::EmojiReactionSerializer
class EmojiReactionSerializer < REST::GroupedEmojiReactionSerializer
attributes :me
def me

View file

@ -333,7 +333,7 @@ Rails.application.routes.draw do
scope module: :statuses do
resources :reblogged_by, controller: :reblogged_by_accounts, only: :index
resources :favourited_by, controller: :favourited_by_accounts, only: :index
resources :emoji_reactioned_by, controller: :emoji_reactioned_by_accounts, only: :index
resources :emoji_reactioned_by, controller: :emoji_reactioned_by, only: :index
resources :mentioned_by, controller: :mentioned_by_accounts, only: :index
resource :reblog, only: :create
post :unreblog, to: 'reblogs#destroy'