mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-23 09:43:08 +02:00
merge: Note Edited notification type (!432)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/432 Closes #403 Approved-by: Amelia Yukii <amelia.yukii@shourai.de> Approved-by: dakkar <dakkar@thenautilus.net>
This commit is contained in:
commit
27ea1b7133
17 changed files with 94 additions and 63 deletions
|
@ -2334,6 +2334,7 @@ _notification:
|
|||
reactedBySomeUsers: "{n} users reacted"
|
||||
renotedBySomeUsers: "Boosted by {n} users"
|
||||
followedBySomeUsers: "Followed by {n} users"
|
||||
edited: "Note got edited"
|
||||
_types:
|
||||
all: "All"
|
||||
note: "New notes"
|
||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -9048,6 +9048,10 @@ export interface Locale extends ILocale {
|
|||
* アンケートの結果が出ました
|
||||
*/
|
||||
"pollEnded": string;
|
||||
/**
|
||||
* 注記が編集されました
|
||||
*/
|
||||
"edited": string;
|
||||
/**
|
||||
* 新しい投稿
|
||||
*/
|
||||
|
|
|
@ -2389,6 +2389,7 @@ _notification:
|
|||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||
pollEnded: "アンケートの結果が出ました"
|
||||
edited: "投稿が編集されました"
|
||||
newNote: "新しい投稿"
|
||||
unreadAntennaNote: "アンテナ {name}"
|
||||
roleAssigned: "ロールが付与されました"
|
||||
|
|
|
@ -96,6 +96,7 @@ export interface MainEventTypes {
|
|||
announcementCreated: {
|
||||
announcement: Packed<'Announcement'>;
|
||||
};
|
||||
edited: Packed<'Note'>;
|
||||
}
|
||||
|
||||
export interface DriveEventTypes {
|
||||
|
|
|
@ -52,7 +52,7 @@ import { isReply } from '@/misc/is-reply.js';
|
|||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited';
|
||||
|
||||
class NotificationManager {
|
||||
private notifier: { id: MiUser['id']; };
|
||||
|
@ -586,7 +586,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// Pack the note
|
||||
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true });
|
||||
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
|
||||
if (data.poll != null) {
|
||||
this.globalEventService.publishNoteStream(note.id, 'updated', {
|
||||
cw: note.cw,
|
||||
|
@ -612,7 +612,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
|
||||
const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note);
|
||||
|
||||
await this.createMentionedEvents(mentionedUsers, note, nm);
|
||||
//await this.createMentionedEvents(mentionedUsers, note, nm);
|
||||
|
||||
// If has in reply to note
|
||||
if (data.reply) {
|
||||
|
@ -634,12 +634,12 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
const muted = isUserRelated(note, userIdsWhoMeMuting);
|
||||
|
||||
if (!isThreadMuted && !muted) {
|
||||
nm.push(data.reply.userId, 'reply');
|
||||
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||
nm.push(data.reply.userId, 'edited');
|
||||
this.globalEventService.publishMainStream(data.reply.userId, 'edited', noteObj);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('edited'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'reply', {
|
||||
this.queueService.webhookDeliver(webhook, 'edited', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
|
@ -647,45 +647,6 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
// If it is renote
|
||||
if (data.renote) {
|
||||
const type = this.isQuote(data) ? 'quote' : 'renote';
|
||||
|
||||
// Notify
|
||||
if (data.renote.userHost === null) {
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
||||
where: {
|
||||
userId: data.renote.userId,
|
||||
threadId: data.renote.threadId ?? data.renote.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
] = data.renote.userId ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(data.renote.userId),
|
||||
]) : [new Set<string>()];
|
||||
|
||||
const muted = isUserRelated(note, userIdsWhoMeMuting);
|
||||
|
||||
if (!isThreadMuted && !muted) {
|
||||
nm.push(data.renote.userId, type);
|
||||
}
|
||||
}
|
||||
|
||||
// Publish event
|
||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
||||
this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'renote', {
|
||||
note: noteObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nm.notify();
|
||||
|
||||
//#region AP deliver
|
||||
|
@ -780,17 +741,17 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
detail: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote);
|
||||
this.globalEventService.publishMainStream(u.id, 'edited', detailPackedNote);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('edited'));
|
||||
for (const webhook of webhooks) {
|
||||
this.queueService.webhookDeliver(webhook, 'mention', {
|
||||
this.queueService.webhookDeliver(webhook, 'edited', {
|
||||
note: detailPackedNote,
|
||||
});
|
||||
}
|
||||
|
||||
// Create notification
|
||||
nm.push(u.id, 'mention');
|
||||
nm.push(u.id, 'edited');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,8 +20,8 @@ import type { OnModuleInit } from '@nestjs/common';
|
|||
import type { UserEntityService } from './UserEntityService.js';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
|
||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]);
|
||||
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded']);
|
||||
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'edited'] as (typeof notificationTypes[number])[]);
|
||||
const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded', 'edited']);
|
||||
|
||||
@Injectable()
|
||||
export class NotificationEntityService implements OnModuleInit {
|
||||
|
|
|
@ -107,6 +107,12 @@ export type MiNotification = {
|
|||
type: 'test';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
} | {
|
||||
type: 'edited';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
notifierId: MiUser['id'];
|
||||
noteId: MiNote['id'];
|
||||
};
|
||||
|
||||
export type MiGroupedNotification = MiNotification | {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
|||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const;
|
||||
|
||||
@Entity('webhook')
|
||||
export class MiWebhook {
|
||||
|
|
|
@ -318,6 +318,31 @@ export const packedNotificationSchema = {
|
|||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['edited'],
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
note: {
|
||||
type: 'object',
|
||||
ref: 'Note',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
|
|
@ -164,7 +164,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
groupedNotifications = groupedNotifications.slice(0, ps.limit);
|
||||
|
||||
const noteIds = groupedNotifications
|
||||
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
|
||||
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type))
|
||||
.map(notification => notification.noteId!);
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
|
|
|
@ -113,7 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
const noteIds = notifications
|
||||
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
|
||||
.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote' | 'edited'> => ['mention', 'reply', 'quote', 'edited'].includes(notification.type))
|
||||
.map(notification => notification.noteId);
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.head">
|
||||
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||
<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
|
||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
|
||||
|
@ -25,8 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||
[$style.t_pollEnded]: notification.type === 'edited',
|
||||
}]"
|
||||
>
|
||||
> <!-- we re-use t_pollEnded for "edited" instead of making an identical style -->
|
||||
<i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i>
|
||||
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ph-clock ph-bold ph-lg"></i>
|
||||
<i v-else-if="notification.type === 'followRequestAccepted'" class="ph-check ph-bold ph-lg"></i>
|
||||
|
@ -40,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||
<i v-else class="ph-seal-check ph-bold ph-lg"></i>
|
||||
</template>
|
||||
<i v-else-if="notification.type === 'edited'" class="ph-pencil ph-bold ph-lg"></i>
|
||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||
<MkReactionIcon
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
|
@ -61,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
||||
<span v-else-if="notification.type === 'edited'">{{ i18n.ts._notification.edited }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
<div>
|
||||
|
@ -131,6 +134,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkA v-else-if="notification.type === 'edited'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||
<i class="ph-quotes ph-bold ph-lg" :class="$style.quote"></i>
|
||||
</MkA>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4273,6 +4273,17 @@ export type components = {
|
|||
body: string;
|
||||
header: string;
|
||||
icon: string;
|
||||
} | {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** @enum {string} */
|
||||
type: 'edited';
|
||||
user: components['schemas']['UserLite'];
|
||||
/** Format: id */
|
||||
userId: string;
|
||||
note: components['schemas']['Note'];
|
||||
} | {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
|
@ -19511,7 +19522,7 @@ export type operations = {
|
|||
url: string;
|
||||
/** @default */
|
||||
secret?: string;
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -19525,7 +19536,7 @@ export type operations = {
|
|||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
name: string;
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
|
||||
url: string;
|
||||
secret: string;
|
||||
active: boolean;
|
||||
|
@ -19584,7 +19595,7 @@ export type operations = {
|
|||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
name: string;
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
|
||||
url: string;
|
||||
secret: string;
|
||||
active: boolean;
|
||||
|
@ -19651,7 +19662,7 @@ export type operations = {
|
|||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
name: string;
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
|
||||
url: string;
|
||||
secret: string;
|
||||
active: boolean;
|
||||
|
@ -19709,7 +19720,7 @@ export type operations = {
|
|||
url: string;
|
||||
/** @default */
|
||||
secret?: string;
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[];
|
||||
on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction' | 'edited')[];
|
||||
active: boolean;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned'] as const;
|
||||
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'achievementEarned', 'edited'] as const;
|
||||
|
||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ export type Channels = {
|
|||
readAntenna: (payload: Antenna) => void;
|
||||
receiveFollowRequest: (payload: User) => void;
|
||||
announcementCreated: (payload: AnnouncementCreated) => void;
|
||||
edited: (payload: Note) => void;
|
||||
};
|
||||
receives: null;
|
||||
};
|
||||
|
|
|
@ -232,6 +232,14 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
|||
data,
|
||||
}];
|
||||
|
||||
case 'edited':
|
||||
return [t('_notification.edited', { name: getUserName(data.body.user) }), {
|
||||
body: data.body.note.text ?? '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
badge: iconUrl('messages'),
|
||||
data,
|
||||
}];
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -133,6 +133,9 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
|||
case 'showFollowRequests':
|
||||
client = await swos.openClient('push', '/my/follow-requests', loginId);
|
||||
break;
|
||||
case 'edited':
|
||||
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
|
||||
break;
|
||||
default:
|
||||
switch (data.body.type) {
|
||||
case 'receiveFollowRequest':
|
||||
|
|
Loading…
Reference in a new issue