From b3597c43addd770f138ab2670287213ef40e4d52 Mon Sep 17 00:00:00 2001 From: noellabo Date: Tue, 14 Sep 2021 16:28:52 +0900 Subject: [PATCH] Add account popup to emoji reactions --- app/javascript/mastodon/actions/accounts.js | 9 +- app/javascript/mastodon/actions/streaming.js | 6 +- .../components/emoji_reactions_bar.js | 142 ++++++++++++++++-- .../mastodon/components/status_action_bar.js | 7 + app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/locales/ja.json | 2 + app/javascript/mastodon/reducers/statuses.js | 4 +- .../styles/mastodon/components.scss | 55 +++++++ app/javascript/styles/mastodon/rtl.scss | 5 + 9 files changed, 216 insertions(+), 16 deletions(-) diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index ebf3f672d..0688ef338 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -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) ) ); }; diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 1ae7912fa..1563aff32 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -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))); diff --git a/app/javascript/mastodon/components/emoji_reactions_bar.js b/app/javascript/mastodon/components/emoji_reactions_bar.js index c1280fabe..2374af399 100644 --- a/app/javascript/mastodon/components/emoji_reactions_bar.js +++ b/app/javascript/mastodon/components/emoji_reactions_bar.js @@ -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 ; + } + + return ( +
+
+ +
+ ); + } +} + +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 ( +
+
+ {accountIds.take(ACCOUNT_POPUP_ROWS_MAX).map(accountId => )} + {accountIds.size > ACCOUNT_POPUP_ROWS_MAX &&
<>{msg}} />
} +
+ ); + } +} 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 ( - + +
+ +
+ {!isUserTouching() && + + + + } +
); - } + }; } @@ -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 ( {items => ( -
+
{items.map(({ key, data, style }) => ( { + 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) { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index c168649a1..aab108cd1 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index c4557eaed..439fd832e 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -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": "引用", diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 9405404b0..20ddbb669 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -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))); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 461917bca..458cfad66 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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; diff --git a/app/javascript/styles/mastodon/rtl.scss b/app/javascript/styles/mastodon/rtl.scss index cd20442c2..94eaa0033 100644 --- a/app/javascript/styles/mastodon/rtl.scss +++ b/app/javascript/styles/mastodon/rtl.scss @@ -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;