Add emoji reaction notification support

This commit is contained in:
KMY 2023-02-26 18:09:25 +09:00
parent dfe1332be6
commit de951a0ef9
11 changed files with 100 additions and 23 deletions

View file

@ -207,7 +207,9 @@ export function emojiReact(status, emoji) {
return function (dispatch, getState) { return function (dispatch, getState) {
dispatch(emojiReactRequest(status, emoji)); dispatch(emojiReactRequest(status, emoji));
api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native }).then(function () { const api_emoji = typeof emoji !== 'string' ? (emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native) : emoji;
api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: api_emoji }).then(function () {
dispatch(emojiReactSuccess(status, emoji)); dispatch(emojiReactSuccess(status, emoji));
}).catch(function (error) { }).catch(function (error) {
dispatch(emojiReactFail(status, emoji, error)); dispatch(emojiReactFail(status, emoji, error));

View file

@ -137,6 +137,7 @@ const excludeTypesFromFilter = filter => {
'follow', 'follow',
'follow_request', 'follow_request',
'favourite', 'favourite',
'emoji_reaction',
'reblog', 'reblog',
'mention', 'mention',
'poll', 'poll',

View file

@ -115,6 +115,17 @@ export default class ColumnSettings extends React.PureComponent {
</div> </div>
</div> </div>
<div role='group' aria-labelledby='notifications-emoji_reaction'>
<span id='notifications-emoji_reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.emoji_reaction' defaultMessage='Emoji Reactions:' /></span>
<div className='column-settings__row'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'emoji_reaction']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'emoji_reaction']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'emoji_reaction']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'emoji_reaction']} onChange={onChange} label={soundStr} />
</div>
</div>
<div role='group' aria-labelledby='notifications-mention'> <div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> <span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>

View file

@ -74,6 +74,13 @@ class FilterBar extends React.PureComponent {
> >
<Icon id='star' fixedWidth /> <Icon id='star' fixedWidth />
</button> </button>
<button
className={selectedFilter === 'emoji_reaction' ? 'active' : ''}
onClick={this.onClick('emoji_reaction')}
title={intl.formatMessage(tooltips.emojiReactions)}
>
<Icon id='star' fixedWidth />
</button>
<button <button
className={selectedFilter === 'reblog' ? 'active' : ''} className={selectedFilter === 'reblog' ? 'active' : ''}
onClick={this.onClick('reblog')} onClick={this.onClick('reblog')}

View file

@ -15,6 +15,7 @@ import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
emojiReaction: { id: 'notification.emoji_reaction', defaultMessage: '{name} reacted your status with emoji' },
follow: { id: 'notification.follow', defaultMessage: '{name} followed you' }, follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' }, ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }, poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
@ -213,6 +214,38 @@ class Notification extends ImmutablePureComponent {
); );
} }
renderEmojiReaction (notification, link) {
const { intl, unread } = this.props;
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-emoji_reaction focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.emojiReaction, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'>
<div className='notification__emoji_reaction-icon-wrapper'>
<Icon id='star' className='star-icon' fixedWidth />
</div>
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.emoji_reaction' defaultMessage='{name} reacted your status with emoji' values={{ name: link }} />
</span>
</div>
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
muted
withDismiss
hidden={!!this.props.hidden}
getScrollPosition={this.props.getScrollPosition}
updateScrollBottom={this.props.updateScrollBottom}
cachedMediaWidth={this.props.cachedMediaWidth}
cacheMediaWidth={this.props.cacheMediaWidth}
/>
</div>
</HotKeys>
);
}
renderReblog (notification, link) { renderReblog (notification, link) {
const { intl, unread } = this.props; const { intl, unread } = this.props;
@ -429,6 +462,8 @@ class Notification extends ImmutablePureComponent {
return this.renderMention(notification); return this.renderMention(notification);
case 'favourite': case 'favourite':
return this.renderFavourite(notification, link); return this.renderFavourite(notification, link);
case 'emoji_reaction':
return this.renderEmojiReaction(notification, link);
case 'reblog': case 'reblog':
return this.renderReblog(notification, link); return this.renderReblog(notification, link);
case 'status': case 'status':

View file

@ -45,11 +45,10 @@ const updateStatusEmojiReaction = (state, emoji_reaction, myId) => {
let emoji_reactions = Array.from(status.get('emoji_reactions') || []); let emoji_reactions = Array.from(status.get('emoji_reactions') || []);
if (emoji_reaction.count > 0) { if (emoji_reaction.count > 0) {
const old_emoji = emoji_reactions.find((er) => er.name === emoji_reaction.name && er.url === emoji_reaction.url); const old_emoji = emoji_reactions.find((er) => er.get('name') === emoji_reaction.name && (!er.get('domain') || er.get('domain') === emoji_reaction.domain));
if (old_emoji) { if (old_emoji) {
old_emoji.account_ids = emoji_reaction.account_ids; const index = emoji_reactions.indexOf(old_emoji);
old_emoji.count = emoji_reaction.count; emoji_reactions[index] = old_emoji.merge({ account_ids: emoji_reaction.account_ids, count: emoji_reaction.count, me: emoji_reaction.me });
old_emoji.me = emoji_reaction.me;
} else { } else {
emoji_reactions.push(ImmutableMap(emoji_reaction)); emoji_reactions.push(ImmutableMap(emoji_reaction));
} }

View file

@ -25,6 +25,7 @@ class Notification < ApplicationRecord
'Follow' => :follow, 'Follow' => :follow,
'FollowRequest' => :follow_request, 'FollowRequest' => :follow_request,
'Favourite' => :favourite, 'Favourite' => :favourite,
'EmojiReaction' => :emoji_reaction,
'Poll' => :poll, 'Poll' => :poll,
}.freeze }.freeze
@ -35,6 +36,7 @@ class Notification < ApplicationRecord
follow follow
follow_request follow_request
favourite favourite
emoji_reaction
poll poll
update update
admin.sign_up admin.sign_up
@ -46,6 +48,7 @@ class Notification < ApplicationRecord
reblog: [status: :reblog], reblog: [status: :reblog],
mention: [mention: :status], mention: [mention: :status],
favourite: [favourite: :status], favourite: [favourite: :status],
emoji_reaction: [emoji_reaction: :status],
poll: [poll: :status], poll: [poll: :status],
update: :status, update: :status,
'admin.report': [report: :target_account], 'admin.report': [report: :target_account],

View file

@ -12,7 +12,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
end end
def status_type? def status_type?
[:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) [:favourite, :emoji_reaction, :reblog, :status, :mention, :poll, :update].include?(object.type)
end end
def report_type? def report_type?

View file

@ -4,14 +4,20 @@ class UnEmojiReactService < BaseService
include Redisable include Redisable
include Payloadable include Payloadable
def call(account, status, emoji_reaction = nil) def call(account_id, status_id, emoji_reaction = nil)
@account_id = account_id
@account = Account.find(account_id)
@status = Status.find(status_id)
if emoji_reaction if emoji_reaction
p '================================ DEBUG2 G'
emoji_reaction.destroy emoji_reaction.destroy
create_notification(emoji_reaction) if !status.account.local? && status.account.activitypub? p '================================ DEBUG2 H'
notify_to_followers(emoji_reaction) if status.account.local? create_notification(emoji_reaction) if !@account.local? && @account.activitypub?
notify_to_followers(emoji_reaction) if @account.local?
write_stream(emoji_reaction) write_stream(emoji_reaction)
else else
bulk(account, status) bulk(@account, @status)
end end
emoji_reaction emoji_reaction
end end
@ -25,33 +31,31 @@ class UnEmojiReactService < BaseService
end end
def create_notification(emoji_reaction) def create_notification(emoji_reaction)
status = emoji_reaction.status ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), @account_id, @account.inbox_url)
ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), status.account_id, status.account.inbox_url)
end end
def notify_to_followers(emoji_reaction) def notify_to_followers(emoji_reaction)
status = emoji_reaction.status ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), @account_id)
ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id)
end end
def write_stream(emoji_reaction) def write_stream(emoji_reaction)
emoji_group = emoji_reaction.status.emoji_reactions_grouped_by_name emoji_group = @status.emoji_reactions_grouped_by_name
.find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) } .find { |reaction_group| reaction_group['name'] == emoji_reaction.name && (!reaction_group.key?(:domain) || reaction_group['domain'] == emoji_reaction.domain) }
if emoji_group if emoji_group
emoji_group['status_id'] = emoji_reaction.status_id.to_s emoji_group['status_id'] = @status.id.to_s
else else
# name: emoji_reaction.name, count: 0, domain: emoji_reaction.domain # name: emoji_reaction.name, count: 0, domain: emoji_reaction.domain
emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => emoji_reaction.status_id.to_s } emoji_group = { 'name' => emoji_reaction.name, 'count' => 0, 'account_ids' => [], 'status_id' => @status.id.to_s }
emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji emoji_group['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
end end
FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), emoji_reaction.status_id, emoji_reaction.account_id) FeedAnyJsonWorker.perform_async(render_emoji_reaction(emoji_group), @status.id, @account_id)
end end
def build_json(emoji_reaction) def build_json(emoji_reaction)
Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoEmojiReactionSerializer)) Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoEmojiReactionSerializer))
end end
def render_emoji_reaction(_emoji_reaction, emoji_group) def render_emoji_reaction(emoji_group)
# @rendered_emoji_reaction ||= InlineRenderer.render(emoji_group, nil, :emoji_reaction) # @rendered_emoji_reaction ||= InlineRenderer.render(emoji_group, nil, :emoji_reaction)
Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json) Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json)
end end

View file

@ -7,14 +7,29 @@ class FeedAnyJsonWorker
include AccountLimitable include AccountLimitable
def perform(payload_json, status_id, my_account_id = nil) def perform(payload_json, status_id, my_account_id = nil)
p '========================================= DEBUG AAA'
redis.publish("timeline:#{my_account_id}", payload_json) if my_account_id.present? redis.publish("timeline:#{my_account_id}", payload_json) if my_account_id.present?
p '========================================= DEBUG AA'
p status_id
p status_id.to_i
status = Status.find(status_id.to_i) status = Status.find(status_id.to_i)
p '========================================= DEBUG AAAAAAAA'
p status.present?
if status.present? if status.present?
scope_status(status).find_each do |account_id| p '========================================= DEBUG A'
p account_id if redis.exists?("subscribed:timeline:#{account_id}") p scope_status(status)
redis.publish("timeline:#{account_id}", payload_json) if redis.exists?("subscribed:timeline:#{account_id}") p '========================================= DEBUG C'
scope_status(status).find_each do |account|
p '========================================= DEBUG D'
p redis.exists?("subscribed:timeline:#{account.id}")
redis.publish("timeline:#{account.id}", payload_json) if redis.exists?("subscribed:timeline:#{account.id}")
end
if status.visibility.to_sym != :public && status.visibility.to_sym != :unlisted && status.account_id != my_account_id &&
redis.exists?("subscribed:timeline:#{status.account_id}")
redis.publish("timeline:#{status.account_id}", payload_json)
end end
end end

View file

@ -12,7 +12,7 @@ class UnEmojiReactWorker
.find { |reaction| domain == '' ? reaction.custom_emoji.nil? : reaction.custom_emoji&.domain == domain } .find { |reaction| domain == '' ? reaction.custom_emoji.nil? : reaction.custom_emoji&.domain == domain }
end end
UnEmojiReactService.new.call(Account.find(account_id), Status.find(status_id), emoji_reaction) UnEmojiReactService.new.call(account_id.to_i, status_id.to_i, emoji_reaction)
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
end end