diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index a3284c73e6..db9b5a2dbd 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -504,3 +504,75 @@ export function toggleFavourite(statusId, skipModal = false) { } }; } + +export const addReaction = (statusId, name, url) => (dispatch, getState) => { + const status = getState().get('statuses').get(statusId); + let alreadyAdded = false; + if (status) { + const reaction = status.get('reactions').find(x => x.get('name') === name); + if (reaction && reaction.get('me')) { + alreadyAdded = true; + } + } + if (!alreadyAdded) { + dispatch(addReactionRequest(statusId, name, url)); + } + + // encodeURIComponent is required for the Keycap Number Sign emoji, see: + // + api(getState).post(`/api/v1/statuses/${statusId}/react/${encodeURIComponent(name)}`).then(() => { + dispatch(addReactionSuccess(statusId, name)); + }).catch(err => { + if (!alreadyAdded) { + dispatch(addReactionFail(statusId, name, err)); + } + }); +}; + +export const addReactionRequest = (statusId, name, url) => ({ + type: REACTION_ADD_REQUEST, + id: statusId, + name, + url, +}); + +export const addReactionSuccess = (statusId, name) => ({ + type: REACTION_ADD_SUCCESS, + id: statusId, + name, +}); + +export const addReactionFail = (statusId, name, error) => ({ + type: REACTION_ADD_FAIL, + id: statusId, + name, + error, +}); + +export const removeReaction = (statusId, name) => (dispatch, getState) => { + dispatch(removeReactionRequest(statusId, name)); + + api(getState).post(`/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(name)}`).then(() => { + dispatch(removeReactionSuccess(statusId, name)); + }).catch(err => { + dispatch(removeReactionFail(statusId, name, err)); + }); +}; + +export const removeReactionRequest = (statusId, name) => ({ + type: REACTION_REMOVE_REQUEST, + id: statusId, + name, +}); + +export const removeReactionSuccess = (statusId, name) => ({ + type: REACTION_REMOVE_SUCCESS, + id: statusId, + name, +}); + +export const removeReactionFail = (statusId, name) => ({ + type: REACTION_REMOVE_FAIL, + id: statusId, + name, +}); diff --git a/app/javascript/flavours/glitch/api_types/notifications.ts b/app/javascript/flavours/glitch/api_types/notifications.ts index ea37556d8d..de9e315ca6 100644 --- a/app/javascript/flavours/glitch/api_types/notifications.ts +++ b/app/javascript/flavours/glitch/api_types/notifications.ts @@ -11,6 +11,7 @@ export const allNotificationTypes = [ 'follow', 'follow_request', 'favourite', + 'reaction', 'reblog', 'mention', 'poll', @@ -24,6 +25,7 @@ export const allNotificationTypes = [ export type NotificationWithStatusType = | 'favourite' + | 'reaction' | 'reblog' | 'status' | 'mention' diff --git a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx index f4275179c5..d43320f394 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/components/notification_group.tsx @@ -15,6 +15,7 @@ import { NotificationFollowRequest } from './notification_follow_request'; import { NotificationMention } from './notification_mention'; import { NotificationModerationWarning } from './notification_moderation_warning'; import { NotificationPoll } from './notification_poll'; +import { NotificationReaction } from './notification_reaction'; import { NotificationReblog } from './notification_reblog'; import { NotificationSeveredRelationships } from './notification_severed_relationships'; import { NotificationStatus } from './notification_status'; @@ -78,6 +79,14 @@ export const NotificationGroup: React.FC<{ /> ); break; + case 'reaction': + content = ( + + ); + break; case 'severed_relationships': content = ( ( + +); + +export const NotificationReaction: React.FC<{ + notification: NotificationGroupReaction; + unread: boolean; +}> = ({ notification, unread }) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx b/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx index 1299796662..59d0effbd1 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx @@ -5,6 +5,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; +import MoodIcon from '@/material-icons/400-24px/mood.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; @@ -23,6 +24,10 @@ const tooltips = defineMessages({ id: 'notifications.filter.favourites', defaultMessage: 'Favorites', }, + reactions: { + id: 'notifications.filter.reactions', + defaultMessage: 'Reactions', + }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, @@ -91,6 +96,14 @@ export const FilterBar: React.FC = () => { > + + + export type NotificationGroupFavourite = BaseNotificationWithStatus<'favourite'>; +export type NotificationGroupReaction = BaseNotificationWithStatus<'reaction'>; export type NotificationGroupReblog = BaseNotificationWithStatus<'reblog'>; export type NotificationGroupStatus = BaseNotificationWithStatus<'status'>; export type NotificationGroupMention = BaseNotificationWithStatus<'mention'>; @@ -76,6 +77,7 @@ export interface NotificationGroupAdminReport export type NotificationGroup = | NotificationGroupFavourite + | NotificationGroupReaction | NotificationGroupReblog | NotificationGroupStatus | NotificationGroupMention @@ -120,6 +122,7 @@ export function createNotificationGroupFromJSON( switch (group.type) { case 'favourite': + case 'reaction': case 'reblog': case 'status': case 'mention': @@ -179,6 +182,7 @@ export function createNotificationGroupFromNotificationJSON( switch (notification.type) { case 'favourite': + case 'reaction': case 'reblog': case 'status': case 'mention': diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index ff4e942aa0..2fc1bfd03b 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -10946,6 +10946,10 @@ noscript { color: $gold-star; } + &--reaction &__icon { + color: $blurple-300; + } + &--reblog &__icon { color: $valid-value-color; } @@ -11119,7 +11123,8 @@ noscript { $icon-margin: 48px; // 40px avatar + 8px gap .status__content, - .status__action-bar { + .status__action-bar, + .reactions-bar { margin-inline-start: $icon-margin; width: calc(100% - $icon-margin); } diff --git a/app/serializers/rest/notification_group_serializer.rb b/app/serializers/rest/notification_group_serializer.rb index 749f717754..0e4efe9278 100644 --- a/app/serializers/rest/notification_group_serializer.rb +++ b/app/serializers/rest/notification_group_serializer.rb @@ -15,7 +15,7 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer def status_type? - [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) + [:favourite, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type) end def report_type? diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 23f92c816b..611894a6df 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -206,7 +206,7 @@ class NotifyService < BaseService private def notification_group_key - return nil if @notification.filtered || %i(favourite reblog).exclude?(@notification.type) + return nil if @notification.filtered || %i(favourite reaction reblog).exclude?(@notification.type) type_prefix = "#{@notification.type}-#{@notification.target_status.id}" redis_key = "notif-group/#{@recipient.id}/#{type_prefix}"