Add account popup to emoji reactions
This commit is contained in:
parent
d10dd36386
commit
b3597c43ad
9 changed files with 216 additions and 16 deletions
|
@ -138,8 +138,13 @@ export function fetchAccountsFromStatuses(statuses) {
|
|||
return fetchAccounts(
|
||||
uniq(
|
||||
statuses
|
||||
.flatMap(item => item.emoji_reactions)
|
||||
.flatMap(emoji_reaction => emoji_reaction.account_ids)
|
||||
.flatMap(status => status.reblog ? status.reblog.emoji_reactions : status.emoji_reactions)
|
||||
.concat(
|
||||
statuses
|
||||
.flatMap(status => status.quote ? status.quote.emoji_reactions : null)
|
||||
)
|
||||
.flatMap(emoji_reaction => emoji_reaction?.account_ids)
|
||||
.filter(e => !!e)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
connectTimeline,
|
||||
disconnectTimeline,
|
||||
} from './timelines';
|
||||
import { fetchRelationships, fetchAccounts } from './accounts';
|
||||
import { getHomeVisibilities } from 'mastodon/selectors';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateConversations } from './conversations';
|
||||
|
@ -94,7 +95,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
dispatch(fetchFilters());
|
||||
break;
|
||||
case 'emoji_reaction':
|
||||
dispatch(updateEmojiReaction(JSON.parse(data.payload)));
|
||||
const emojiReaction = JSON.parse(data.payload);
|
||||
dispatch(fetchRelationships(emojiReaction.account_ids));
|
||||
dispatch(fetchAccounts(emojiReaction.account_ids));
|
||||
dispatch(updateEmojiReaction(emojiReaction));
|
||||
break;
|
||||
case 'announcement':
|
||||
dispatch(updateAnnouncements(JSON.parse(data.payload)));
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from 'mastodon/selectors';
|
||||
import ImmutablePropTypes, { list } from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { List } from 'immutable';
|
||||
import classNames from 'classnames';
|
||||
import Emoji from './emoji';
|
||||
import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light';
|
||||
|
@ -9,6 +12,85 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
|||
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||
import { reduceMotion, me } from 'mastodon/initial_state';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { isUserTouching } from 'mastodon/is_mobile';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
@connect(makeMapStateToProps)
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
|
||||
if ( !account ) {
|
||||
return <Fragment></Fragment>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-popup__wapper'>
|
||||
<div className='account-popup__avatar-wrapper'><Avatar account={account} size={14} /></div>
|
||||
<bdi><strong className='account-popup__display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ACCOUNT_POPUP_ROWS_MAX = 10;
|
||||
|
||||
@injectIntl
|
||||
class AccountPopup extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { accountIds, placement } = this.props;
|
||||
var { arrowOffsetLeft, arrowOffsetTop, style } = this.props;
|
||||
const OFFSET = 6;
|
||||
|
||||
if (placement === 'top') {
|
||||
arrowOffsetTop = String(parseInt(arrowOffsetTop ?? '0') - OFFSET);
|
||||
style = { ...style, top: style.top - OFFSET };
|
||||
} else if (placement === 'bottom') {
|
||||
arrowOffsetTop = String(parseInt(arrowOffsetTop ?? '0') + OFFSET);
|
||||
style = { ...style, top: style.top + OFFSET };
|
||||
} else if (placement === 'left') {
|
||||
arrowOffsetLeft = String(parseInt(arrowOffsetLeft ?? '0') - OFFSET);
|
||||
style = { ...style, left: style.left - OFFSET };
|
||||
} else if (placement === 'right') {
|
||||
arrowOffsetLeft = String(parseInt(arrowOffsetLeft ?? '0') + OFFSET);
|
||||
style = { ...style, left: style.left + OFFSET };
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`dropdown-menu account-popup ${placement}`} style={{ ...style}}>
|
||||
<div className={`dropdown-menu__arrow account-popup__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||
{accountIds.take(ACCOUNT_POPUP_ROWS_MAX).map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
{accountIds.size > ACCOUNT_POPUP_ROWS_MAX && <div className='account-popup__wapper'><bdi><strong className='account-popup__display-name__html'><FormattedMessage id='account_popup.more_users' defaultMessage='({number, plural, one {# other user} other {# other users}})' values={{ number: accountIds.size - ACCOUNT_POPUP_ROWS_MAX}} children={msg=> <>{msg}</>} /></strong></bdi></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EmojiReaction extends ImmutablePureComponent {
|
||||
|
||||
|
@ -34,11 +116,40 @@ class EmojiReaction extends ImmutablePureComponent {
|
|||
} else {
|
||||
addEmojiReaction(status, emojiReaction.get('name'), emojiReaction.get('domain', null), emojiReaction.get('url', null), emojiReaction.get('static_url', null));
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseEnter = ({ target }) => {
|
||||
const { top } = target.getBoundingClientRect();
|
||||
|
||||
this.setState({
|
||||
hovered: true,
|
||||
placement: top * 2 < innerHeight ? 'bottom' : 'top',
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({
|
||||
hovered: false,
|
||||
});
|
||||
};
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.target?.addEventListener('mouseenter', this.handleMouseEnter, { capture: true });
|
||||
this.target?.addEventListener('mouseleave', this.handleMouseLeave, false);
|
||||
}
|
||||
|
||||
handleMouseEnter = () => this.setState({ hovered: true })
|
||||
|
||||
handleMouseLeave = () => this.setState({ hovered: false })
|
||||
componentWillUnmount () {
|
||||
this.target?.removeEventListener('mouseenter', this.handleMouseEnter, { capture: true });
|
||||
this.target?.removeEventListener('mouseleave', this.handleMouseLeave, false);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { emojiReaction, status, myReaction } = this.props;
|
||||
|
@ -50,12 +161,21 @@ class EmojiReaction extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={status.get('emoji_reactioned') && !myReaction} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
|
||||
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={this.props.emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
|
||||
<span className='reactions-bar__item__count'><AnimatedNumber value={emojiReaction.get('count')} /></span>
|
||||
</button>
|
||||
<Fragment>
|
||||
<div className='reactions-bar__item-wrapper' ref={this.setTargetRef}>
|
||||
<button className={classNames('reactions-bar__item', { active: myReaction })} disabled={status.get('emoji_reactioned') && !myReaction} onClick={this.handleClick} title={`:${shortCode}:`} style={this.props.style}>
|
||||
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={emojiReaction.get('name')} emojiMap={this.props.emojiMap} url={emojiReaction.get('url')} static_url={emojiReaction.get('static_url')} /></span>
|
||||
<span className='reactions-bar__item__count'><AnimatedNumber value={emojiReaction.get('count')} /></span>
|
||||
</button>
|
||||
</div>
|
||||
{!isUserTouching() &&
|
||||
<Overlay show={this.state.hovered} placement={this.state.placement} target={this.findTarget}>
|
||||
<AccountPopup accountIds={emojiReaction.get('account_ids', List())} />
|
||||
</Overlay>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
@ -92,7 +212,7 @@ export default class EmojiReactionsBar extends ImmutablePureComponent {
|
|||
key: `${emoji_reaction.get('name')}${domain ? `@${domain}` : ''}`,
|
||||
data: {
|
||||
emojiReaction: emoji_reaction,
|
||||
myReaction: emoji_reaction.get('account_ids', []).includes(me),
|
||||
myReaction: emoji_reaction.get('account_ids', List()).includes(me),
|
||||
},
|
||||
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
||||
};
|
||||
|
@ -101,7 +221,7 @@ export default class EmojiReactionsBar extends ImmutablePureComponent {
|
|||
return (
|
||||
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
||||
{items => (
|
||||
<div className='reactions-bar'>
|
||||
<div className='reactions-bar emoji-reactions-bar'>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<EmojiReaction
|
||||
key={key}
|
||||
|
|
|
@ -34,6 +34,7 @@ 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' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
|
@ -224,6 +225,10 @@ 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);
|
||||
}
|
||||
|
@ -280,6 +285,8 @@ 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 });
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (!show_bookmark_button || writtenByMe && publicStatus && !expired) {
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"account.unmute": "Unmute @{name}",
|
||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||
"account_note.placeholder": "Click to add note",
|
||||
"account_popup.more_users": "({number, plural, one {# other user} other {# other users}})",
|
||||
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
|
||||
"alert.rate_limited.title": "Rate limited",
|
||||
"alert.unexpected.message": "An unexpected error occurred.",
|
||||
|
@ -505,6 +506,7 @@
|
|||
"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",
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
"account.unmute": "@{name}さんのミュートを解除",
|
||||
"account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
|
||||
"account_note.placeholder": "クリックしてメモを追加",
|
||||
"account_popup.more_users": "(他、{number}人のユーザー)",
|
||||
"alert.rate_limited.message": "{retry_time, time, medium} 以降に再度実行してください。",
|
||||
"alert.rate_limited.title": "制限に達しました",
|
||||
"alert.unexpected.message": "不明なエラーが発生しました。",
|
||||
|
@ -506,6 +507,7 @@
|
|||
"status.mute_conversation": "会話をミュート",
|
||||
"status.muted_quote": "ミュートされた引用",
|
||||
"status.open": "詳細を表示",
|
||||
"status.open_emoji_reactions": "この投稿への絵文字リアクション一覧",
|
||||
"status.pin": "プロフィールに固定表示",
|
||||
"status.pinned": "固定された投稿",
|
||||
"status.quote": "引用",
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
|
||||
import { me } from '../initial_state';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap, List, fromJS } from 'immutable';
|
||||
|
||||
const importStatus = (state, status) => {
|
||||
if (state.getIn([status.in_reply_to_id, 'replies_count'], null) == 0) {
|
||||
|
@ -60,7 +60,7 @@ const updateEmojiReaction = (state, id, name, domain, url, static_url, updater)
|
|||
});
|
||||
});
|
||||
|
||||
const updateEmojiReactionCount = (state, emojiReaction) => updateEmojiReaction(state, emojiReaction.status_id, emojiReaction.name, emojiReaction.domain, emojiReaction.url, emojiReaction.static_url, x => x.set('count', emojiReaction.count).set('account_ids', emojiReaction.account_ids));
|
||||
const updateEmojiReactionCount = (state, emojiReaction) => updateEmojiReaction(state, emojiReaction.status_id, emojiReaction.name, emojiReaction.domain, emojiReaction.url, emojiReaction.static_url, x => x.set('count', emojiReaction.count).set('account_ids', new List(emojiReaction.account_ids)));
|
||||
|
||||
const addEmojiReaction = (state, id, name, domain, url, static_url) => updateEmojiReaction(state, id, name, domain, url, static_url, x => x.update('count', y => y + 1).update('account_ids', z => z.push(me)));
|
||||
|
||||
|
|
|
@ -1471,6 +1471,56 @@
|
|||
}
|
||||
}
|
||||
|
||||
.account-popup {
|
||||
max-width: min(100vw, 600px);
|
||||
|
||||
&.dropdown-menu {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&__arrow.dropdown-menu__arrow {
|
||||
&.left {
|
||||
border-left-color: $white;
|
||||
}
|
||||
|
||||
&.top {
|
||||
border-top-color: $white;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
border-bottom-color: $white;
|
||||
}
|
||||
|
||||
&.right {
|
||||
border-right-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&__wapper {
|
||||
padding: 2px 6px;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&__avatar-wrapper,
|
||||
&__display-name__html {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&__avatar-wrapper {
|
||||
padding-left: 0;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
&__display-name__html {
|
||||
color: $black;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.follow-recommendations-account {
|
||||
.icon-button {
|
||||
color: $ui-primary-color;
|
||||
|
@ -7843,6 +7893,11 @@ noscript {
|
|||
}
|
||||
}
|
||||
|
||||
.emoji-reactions-bar.reactions-bar {
|
||||
margin-left: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.notification,
|
||||
.status__wrapper {
|
||||
position: relative;
|
||||
|
|
|
@ -119,6 +119,11 @@ body.rtl {
|
|||
float: right;
|
||||
}
|
||||
|
||||
.account-popup__avatar-wrapper {
|
||||
padding-left: 4px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.column-header__back-button {
|
||||
padding-left: 5px;
|
||||
padding-right: 0;
|
||||
|
|
Loading…
Reference in a new issue