Skip to content

Commit

Permalink
Change: 絵文字リアクションの通知のグループ化で、アカウントを絵文字の種類ごとに表示 (#796)
Browse files Browse the repository at this point in the history
* Change: 絵文字リアクションの通知のグループ化で、アカウントを絵文字の種類ごとに表示

* Fix lint

* アカウントの一括取得数を制限

* ストリーミング対応

* Fix

* Fix

* Fix

* Fix some problems

* Fix
  • Loading branch information
kmycode authored Aug 16, 2024
1 parent 5dec110 commit f14c2d3
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 7 deletions.
19 changes: 19 additions & 0 deletions app/javascript/mastodon/api_types/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,29 @@ export type NotificationType =
| 'admin.sign_up'
| 'admin.report';

export interface NotifyEmojiReactionJSON {
name: string;
count: number;
me: boolean;
url?: string;
static_url?: string;
domain?: string;
width?: number;
height?: number;
}

export interface NotificationEmojiReactionGroupJSON {
emoji_reaction: NotifyEmojiReactionJSON;
sample_account_ids: string[];
}

export interface BaseNotificationJSON {
id: string;
type: NotificationType;
created_at: string;
group_key: string;
account: ApiAccountJSON;
emoji_reaction?: NotifyEmojiReactionJSON;
}

export interface BaseNotificationGroupJSON {
Expand All @@ -60,6 +77,7 @@ export interface BaseNotificationGroupJSON {
most_recent_notification_id: string;
page_min_id?: string;
page_max_id?: string;
emoji_reaction_groups?: NotificationEmojiReactionGroupJSON[];
}

interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
Expand All @@ -70,6 +88,7 @@ interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
interface NotificationWithStatusJSON extends BaseNotificationJSON {
type: NotificationWithStatusType;
status: ApiStatusJSON;
emoji_reaction?: NotifyEmojiReactionJSON;
}

interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const NotificationEmojiReaction: React.FC<{
icon={EmojiReactionIcon}
iconId='star'
accountIds={notification.sampleAccountIds}
emojiReactionGroups={notification.emojiReactionGroups}
statusId={notification.statusId}
timestamp={notification.latest_page_notification_at}
count={notification.notifications_count}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { HotKeys } from 'react-hotkeys';

import { replyComposeById } from 'mastodon/actions/compose';
import { navigateToStatus } from 'mastodon/actions/statuses';
import EmojiView from 'mastodon/components/emoji_view';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import type { EmojiReactionGroup } from 'mastodon/models/notification_group';
import { useAppDispatch } from 'mastodon/store';

import { AvatarGroup } from './avatar_group';
Expand All @@ -26,6 +28,7 @@ export const NotificationGroupWithStatus: React.FC<{
actions?: JSX.Element;
count: number;
accountIds: string[];
emojiReactionGroups?: EmojiReactionGroup[];
timestamp: string;
labelRenderer: LabelRenderer;
labelSeeMoreHref?: string;
Expand All @@ -36,6 +39,7 @@ export const NotificationGroupWithStatus: React.FC<{
iconId,
timestamp,
accountIds,
emojiReactionGroups,
actions,
count,
statusId,
Expand Down Expand Up @@ -89,11 +93,28 @@ export const NotificationGroupWithStatus: React.FC<{

<div className='notification-group__main'>
<div className='notification-group__main__header'>
<div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />
{emojiReactionGroups?.map((group) => (
<div key={group.emoji.name}>
<div className='notification-group__main__header__wrapper__for_emoji_reaction'>
<EmojiView
name={group.emoji.name}
url={group.emoji.url}
staticUrl={group.emoji.static_url}
/>
<AvatarGroup accountIds={group.sampleAccountIds} />

{actions}
</div>
{actions}
</div>
</div>
))}

{!emojiReactionGroups && (
<div className='notification-group__main__header__wrapper'>
<AvatarGroup accountIds={accountIds} />

{actions}
</div>
)}

<div className='notification-group__main__header__label'>
{label}
Expand Down
67 changes: 65 additions & 2 deletions app/javascript/mastodon/models/notification_group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
ApiNotificationJSON,
NotificationType,
NotificationWithStatusType,
NotificationEmojiReactionGroupJSON,
NotifyEmojiReactionJSON,
} from 'mastodon/api_types/notifications';
import type { ApiReportJSON } from 'mastodon/api_types/reports';

Expand All @@ -22,6 +24,23 @@ interface BaseNotificationWithStatus<Type extends NotificationWithStatusType>
extends BaseNotificationGroup {
type: Type;
statusId: string;
emojiReactionGroups?: EmojiReactionGroup[];
}

interface EmojiInfo {
name: string;
count: number;
me: boolean;
url?: string;
static_url?: string;
domain?: string;
width?: number;
height?: number;
}

export interface EmojiReactionGroup {
emoji: EmojiInfo;
sampleAccountIds: string[];
}

interface BaseNotification<Type extends NotificationType>
Expand Down Expand Up @@ -119,14 +138,27 @@ function createAccountRelationshipSeveranceEventFromJSON(
return eventJson;
}

function createEmojiReactionGroupsFromJSON(
json: NotifyEmojiReactionJSON | undefined,
sampleAccountIds: string[],
): EmojiReactionGroup[] {
if (typeof json === 'undefined') return [];

return [
{
emoji: json,
sampleAccountIds,
},
];
}

export function createNotificationGroupFromJSON(
groupJson: ApiNotificationGroupJSON,
): NotificationGroup {
const { sample_account_ids: sampleAccountIds, ...group } = groupJson;

switch (group.type) {
case 'favourite':
case 'emoji_reaction':
case 'reblog':
case 'status':
case 'mention':
Expand All @@ -140,6 +172,29 @@ export function createNotificationGroupFromJSON(
...groupWithoutStatus,
};
}
case 'emoji_reaction': {
const {
status_id: statusId,
emoji_reaction_groups: emojiReactionGroups,
...groupWithoutStatus
} = group;
const groups = (
typeof emojiReactionGroups === 'undefined'
? ([] as NotificationEmojiReactionGroupJSON[])
: emojiReactionGroups
).map((g) => {
return {
sampleAccountIds: g.sample_account_ids,
emoji: g.emoji_reaction,
} as EmojiReactionGroup;
});
return {
statusId,
sampleAccountIds,
emojiReactionGroups: groups,
...groupWithoutStatus,
};
}
case 'admin.report': {
const { report, ...groupWithoutTargetAccount } = group;
return {
Expand Down Expand Up @@ -187,14 +242,22 @@ export function createNotificationGroupFromNotificationJSON(

switch (notification.type) {
case 'favourite':
case 'emoji_reaction':
case 'reblog':
case 'status':
case 'mention':
case 'status_reference':
case 'poll':
case 'update':
return { ...group, statusId: notification.status.id };
case 'emoji_reaction':
return {
...group,
statusId: notification.status.id,
emojiReactionGroups: createEmojiReactionGroupsFromJSON(
notification.emoji_reaction,
group.sampleAccountIds,
),
};
case 'admin.report':
return { ...group, report: createReportFromJSON(notification.report) };
case 'severed_relationships':
Expand Down
35 changes: 35 additions & 0 deletions app/javascript/mastodon/reducers/notification_groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,41 @@ function processNewNotification(
if (existingGroupIndex > -1) {
const existingGroup = groups[existingGroupIndex];

if (existingGroup && existingGroup.type !== 'gap') {
// Update emoji reaction emoji groups
if (existingGroup.type === 'emoji_reaction') {
const emojiReactionGroups = existingGroup.emojiReactionGroups;
const emojiReactionData = notification.emoji_reaction;

if (emojiReactionGroups && emojiReactionData) {
const sameEmojiIndex = emojiReactionGroups.findIndex(
(g) => g.emoji.name === emojiReactionData.name,
);

if (sameEmojiIndex > -1) {
const sameEmoji = emojiReactionGroups[sameEmojiIndex];

if (sameEmoji) {
if (
!sameEmoji.sampleAccountIds.includes(notification.account.id) &&
sameEmoji.sampleAccountIds.unshift(notification.account.id) >
NOTIFICATIONS_GROUP_MAX_AVATARS
)
sameEmoji.sampleAccountIds.pop();

emojiReactionGroups.splice(sameEmojiIndex, 1);
emojiReactionGroups.unshift(sameEmoji);
}
} else {
emojiReactionGroups.unshift({
emoji: emojiReactionData,
sampleAccountIds: [notification.account.id],
});
}
}
}
}

if (
existingGroup &&
existingGroup.type !== 'gap' &&
Expand Down
19 changes: 19 additions & 0 deletions app/javascript/styles/mastodon/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10907,6 +10907,25 @@ noscript {
&__wrapper {
display: flex;
justify-content: space-between;

&__for_emoji_reaction {
display: flex;
justify-content: start;

.emoji {
display: inline-block;
width: 40px;
align-self: center;

img {
height: 20px;
}
}

.notification-group__avatar-group {
margin-left: 8px;
}
}
}

&__label {
Expand Down
24 changes: 23 additions & 1 deletion app/models/notification_group.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# frozen_string_literal: true

class NotificationGroup < ActiveModelSerializers::Model
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id
attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id, :emoji_reaction_groups

# Try to keep this consistent with `app/javascript/mastodon/models/notification_group.ts`
SAMPLE_ACCOUNTS_SIZE = 8
SAMPLE_ACCOUNTS_SIZE_FOR_EMOJI_REACTION = 40

class NotificationEmojiReactionGroup < ActiveModelSerializers::Model
attributes :emoji_reaction, :sample_accounts
end

def self.from_notification(notification, max_id: nil)
if notification.group_key.present?
Expand All @@ -16,17 +21,22 @@ def self.from_notification(notification, max_id: nil)
most_recent_notifications = scope.order(id: :desc).includes(:from_account).take(SAMPLE_ACCOUNTS_SIZE)
most_recent_id = most_recent_notifications.first.id
sample_accounts = most_recent_notifications.map(&:from_account)
emoji_reaction_groups = extract_emoji_reaction_pair(
scope.order(id: :desc).includes(emoji_reaction: :account).take(SAMPLE_ACCOUNTS_SIZE_FOR_EMOJI_REACTION)
)
notifications_count = scope.count
else
most_recent_id = notification.id
sample_accounts = [notification.from_account]
emoji_reaction_groups = extract_emoji_reaction_pair([notification])
notifications_count = 1
end

NotificationGroup.new(
notification: notification,
group_key: notification.group_key || "ungrouped-#{notification.id}",
sample_accounts: sample_accounts,
emoji_reaction_groups: emoji_reaction_groups,
notifications_count: notifications_count,
most_recent_notification_id: most_recent_id
)
Expand All @@ -38,4 +48,16 @@ def self.from_notification(notification, max_id: nil)
:account_relationship_severance_event,
:account_warning,
to: :notification, prefix: false

def self.extract_emoji_reaction_pair(scope)
scope = scope.filter { |g| g.emoji_reaction.present? }

return [] if scope.empty?
return [] unless scope.first.type == :emoji_reaction

scope
.each_with_object({}) { |e, h| h[e.emoji_reaction.name] = (h[e.emoji_reaction.name] || []).push(e.emoji_reaction) }
.to_a
.map { |pair| NotificationEmojiReactionGroup.new(emoji_reaction: pair[1].first, sample_accounts: pair[1].take(SAMPLE_ACCOUNTS_SIZE).map(&:account)) }
end
end
15 changes: 15 additions & 0 deletions app/serializers/rest/notification_group_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def report_type?
object.type == :'admin.report'
end

def emoji_reaction_type?
object.type == :emoji_reaction
end

def relationship_severance_event?
object.type == :severed_relationships
end
Expand All @@ -56,4 +60,15 @@ def latest_page_notification_at
def paginated?
!instance_options[:group_metadata].nil?
end

class NotificationEmojiReactionGroupSerializer < ActiveModel::Serializer
has_one :emoji_reaction, serializer: REST::NotifyEmojiReactionSerializer
attribute :sample_account_ids

def sample_account_ids
object.sample_accounts.pluck(:id).map(&:to_s)
end
end

has_many :emoji_reaction_groups, each_serializer: NotificationEmojiReactionGroupSerializer, if: :emoji_reaction_type?
end
1 change: 1 addition & 0 deletions app/serializers/rest/notify_emoji_reaction_serializer.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

class REST::NotifyEmojiReactionSerializer < ActiveModel::Serializer
# Please update app/javascript/api_types/notification.ts when making changes to the attributes
include RoutingHelper

attributes :name
Expand Down
Loading

0 comments on commit f14c2d3

Please sign in to comment.