Create new stream event type: emoji_reaction

This commit is contained in:
KMY 2023-02-26 09:45:22 +09:00
parent 14fddebbd0
commit 1c028a20ac
15 changed files with 171 additions and 15 deletions

View file

@ -207,8 +207,7 @@ export function emojiReact(status, emoji) {
return function (dispatch, getState) {
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 (response) {
dispatch(importFetchedStatus(response.data));
api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_reactions`, { emoji: emoji.custom ? (emoji.name + (emoji.domain || '')) : emoji.native }).then(function () {
dispatch(emojiReactSuccess(status, emoji));
}).catch(function (error) {
dispatch(emojiReactFail(status, emoji, error));
@ -220,8 +219,7 @@ export function unEmojiReact(status, emoji) {
return (dispatch, getState) => {
dispatch(unEmojiReactRequest(status, emoji));
api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`, { emoji }).then(response => {
dispatch(importFetchedStatus(response.data));
api(getState).post(`/api/v1/statuses/${status.get('id')}/emoji_unreaction`, { emoji }).then(() => {
dispatch(unEmojiReactSuccess(status, emoji));
}).catch(error => {
dispatch(unEmojiReactFail(status, emoji, error));

View file

@ -15,6 +15,7 @@ import { unescapeHTML } from '../utils/html';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import compareId from 'mastodon/compare_id';
import { requestNotificationPermission } from '../utils/notifications';
import { STATUS_EMOJI_REACTION_UPDATE } from './statuses';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
@ -54,6 +55,15 @@ export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING,
});
export function updateEmojiReactions(emoji_reaction, accountId) {
return (dispatch) =>
dispatch({
type: STATUS_EMOJI_REACTION_UPDATE,
emoji_reaction,
accountId,
});
}
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);

View file

@ -39,6 +39,8 @@ export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
export const STATUS_EMOJI_REACTION_UPDATE = 'STATUS_EMOJI_REACTION_UPDATE';
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
@ -347,3 +349,8 @@ export const undoStatusTranslation = id => ({
type: STATUS_TRANSLATE_UNDO,
id,
});
export const updateEmojiReaction = (emoji_reaction) => ({
type: STATUS_EMOJI_REACTION_UPDATE,
emoji_reaction,
});

View file

@ -12,7 +12,7 @@ import {
fillCommunityTimelineGaps,
fillListTimelineGaps,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications';
import { updateConversations } from './conversations';
import { updateStatus } from './statuses';
import {
@ -93,6 +93,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
case 'emoji_reaction':
dispatch(updateEmojiReactions(JSON.parse(data.payload), getState().getIn(['meta', 'me'])));
break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));
break;

View file

@ -14,7 +14,6 @@ class EmojiReactionButton extends React.PureComponent {
staticUrl: PropTypes.string,
count: PropTypes.number.isRequired,
me: PropTypes.bool,
status: PropTypes.map,
onEmojiReact: PropTypes.func,
onUnEmojiReact: PropTypes.func,
};

View file

@ -396,6 +396,7 @@
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.",
"notification.admin.report": "{name} reported {target}",
"notification.admin.sign_up": "{name} signed up",
"notification.emoji_reaction": "{name} reacted your post with emoji",
"notification.favourite": "{name} favourited your post",
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",

View file

@ -396,6 +396,7 @@
"not_signed_in_indicator.not_signed_in": "この機能を使うにはログインする必要があります。",
"notification.admin.report": "{name}さんが{target}さんを通報しました",
"notification.admin.sign_up": "{name}さんがサインアップしました",
"notification.emoji_reaction": "{name}さんがあなたの投稿に絵文字をつけました",
"notification.favourite": "{name}さんがあなたの投稿をお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました",
"notification.follow_request": "{name}さんがあなたにフォローリクエストしました",

View file

@ -17,10 +17,11 @@ import {
STATUS_TRANSLATE_UNDO,
STATUS_FETCH_REQUEST,
STATUS_FETCH_FAIL,
STATUS_EMOJI_REACTION_UPDATE,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
const importStatus = (state, status) => state.set(status.id, fromJS(status));
@ -35,6 +36,30 @@ const deleteStatus = (state, id, references) => {
return state.delete(id);
};
const updateStatusEmojiReaction = (state, emoji_reaction, myId) => {
emoji_reaction.me = emoji_reaction.account_ids ? emoji_reaction.account_ids.indexOf(myId) >= 0 : false;
const status = state.get(emoji_reaction.status_id);
if (!status) return state;
let emoji_reactions = Array.from(status.get('emoji_reactions') || []);
if (emoji_reaction.count > 0) {
const old_emoji = emoji_reactions.find((er) => er.name === emoji_reaction.name && er.url === emoji_reaction.url);
if (old_emoji) {
old_emoji.account_ids = emoji_reaction.account_ids;
old_emoji.count = emoji_reaction.count;
old_emoji.me = emoji_reaction.me;
} else {
emoji_reactions.push(ImmutableMap(emoji_reaction));
}
} else {
emoji_reactions = emoji_reactions.filter((er) => er.get('name') !== emoji_reaction.name || er.get('domain') !== emoji_reaction.domain);
}
return state.setIn([emoji_reaction.status_id, 'emoji_reactions'], ImmutableList(emoji_reactions));
};
const initialState = ImmutableMap();
export default function statuses(state = initialState, action) {
@ -89,6 +114,8 @@ export default function statuses(state = initialState, action) {
return state.setIn([action.id, 'translation'], fromJS(action.translation));
case STATUS_TRANSLATE_UNDO:
return state.deleteIn([action.id, 'translation']);
case STATUS_EMOJI_REACTION_UPDATE:
return updateStatusEmojiReaction(state, action.emoji_reaction, action.accountId);
default:
return state;
}

View file

@ -1264,7 +1264,8 @@ body > [data-popper-placement] {
.status__emoji-reactions-bar {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 4px 8px;
margin: 8px 0 2px 4px;
.emoji-reactions-bar__button {

View file

@ -14,6 +14,8 @@ class InlineRenderer
preload_associations_for_status
when :notification
serializer = REST::NotificationSerializer
when :emoji_reaction
serializer = REST::EmojiReactionSerializer
when :conversation
serializer = REST::ConversationSerializer
when :announcement

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
class ActivityPub::EmojiReactionSerializer < ActivityPub::Serializer
attributes :id, :type, :actor, :content
attribute :virtual_object, key: :object
has_many :virtual_tags, key: :tag, unless: -> { object.custom_emoji.nil? }
def id
[ActivityPub::TagManager.instance.uri_for(object.account), '#likes/', object.id].join
end
def type
'Like'
end
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end
def virtual_object
ActivityPub::TagManager.instance.uri_for(object.status)
end
def content
object.custom_emoji.nil? ? object.name : ":#{object.name}:"
end
def virtual_tags
[object.custom_emoji]
end
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class ActivityPub::UndoEmojiReactionSerializer < ActivityPub::Serializer
attributes :id, :type, :actor, :content
has_one :object, serializer: ActivityPub::EmojiReactionSerializer
def id
[ActivityPub::TagManager.instance.uri_for(object.account), '#emoji_reactions/', object.id, '/undo'].join
end
def type
'Undo'
end
def actor
ActivityPub::TagManager.instance.uri_for(object.account)
end
def content
object.custom_emoji.nil? ? object.name : ":#{object.name}:"
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class REST::EmojiReactionSerializer < ActiveModel::Serializer
attributes :name, :count
attribute :url, if: :custom_emoji?
attribute :static_url, if: :custom_emoji?
attribute :domain, if: :custom_emoji?
attribute :account_ids, if: :account_ids?
def custom_emoji?
object.url.present?
end
def account_ids?
object.respond_to?(:account_ids)
end
end

View file

@ -3,6 +3,7 @@
class EmojiReactService < BaseService
include Authorization
include Payloadable
include Redisable
# React a status with emoji and notify remote user
# @param [Account] account
@ -27,6 +28,7 @@ class EmojiReactService < BaseService
create_notification(emoji_reaction)
notify_to_followers(emoji_reaction)
bump_potential_friendship(account, status)
write_stream(emoji_reaction)
emoji_reaction
end
@ -37,8 +39,7 @@ class EmojiReactService < BaseService
status = emoji_reaction.status
if status.account.local?
# TODO: Change favourite event to notify
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'Favourite', 'favourite')
LocalNotificationWorker.perform_async(status.account_id, emoji_reaction.id, 'EmojiReaction', 'emoji_reaction')
elsif status.account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(emoji_reaction), emoji_reaction.account_id, status.account.inbox_url)
end
@ -58,6 +59,13 @@ class EmojiReactService < BaseService
DistributionWorker.perform_async(status.id, { 'update' => true })
end
def write_stream(emoji_reaction)
emoji_group = emoji_reaction.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) }
emoji_group['status_id'] = emoji_reaction.status_id.to_s
redis.publish("timeline:#{emoji_reaction.status.account_id}", render_emoji_reaction(emoji_group))
end
def bump_potential_friendship(account, status)
ActivityTracker.increment('activity:interactions')
return if account.following?(status.account_id)
@ -66,7 +74,11 @@ class EmojiReactService < BaseService
end
def build_json(emoji_reaction)
# TODO: change to original serializer for other servers
Oj.dump(serialize_payload(emoji_reaction, ActivityPub::LikeSerializer))
Oj.dump(serialize_payload(emoji_reaction, ActivityPub::EmojiReactionSerializer))
end
def render_emoji_reaction(emoji_group)
# @rendered_emoji_reaction ||= InlineRenderer.render(HashObject.new(emoji_group), nil, :emoji_reaction)
@render_emoji_reaction ||= Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json)
end
end

View file

@ -1,13 +1,15 @@
# frozen_string_literal: true
class UnEmojiReactService < BaseService
include Redisable
include Payloadable
def call(account, status, emoji_reaction = nil)
if emoji_reaction
emoji_reaction.destroy!
emoji_reaction.destroy
create_notification(emoji_reaction) if !status.account.local? && status.account.activitypub?
notify_to_followers(emoji_reaction) if status.account.local?
write_stream(emoji_reaction)
else
bulk(account, status)
end
@ -32,8 +34,25 @@ class UnEmojiReactService < BaseService
ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id)
end
def write_stream(emoji_reaction)
emoji_group = emoji_reaction.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) }
if emoji_group
emoji_group['status_id'] = emoji_reaction.status_id.to_s
else
# 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['domain'] = emoji_reaction.custom_emoji.domain if emoji_reaction.custom_emoji
end
redis.publish("timeline:#{emoji_reaction.status.account_id}", render_emoji_reaction(emoji_reaction, emoji_group))
end
def build_json(emoji_reaction)
# TODO: change to original serializer for other servers
Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoLikeSerializer))
Oj.dump(serialize_payload(emoji_reaction, ActivityPub::UndoEmojiReactionSerializer))
end
def render_emoji_reaction(_emoji_reaction, emoji_group)
# @rendered_emoji_reaction ||= InlineRenderer.render(emoji_group, nil, :emoji_reaction)
Oj.dump(event: :emoji_reaction, payload: emoji_group.to_json)
end
end