Merge pull request #35 from lqvp/master

フォロー解除時にも通知が来るように
This commit is contained in:
ひたりん 2024-10-01 05:46:05 +09:00 committed by hijiki
parent 10be4267b1
commit 796492e3bf
14 changed files with 122 additions and 2 deletions

View File

@ -1,4 +1,5 @@
# DIFFRENCE # DIFFRENCE
<<<<<<< HEAD
## 2024.9.0-yami-1.3.1 ## 2024.9.0-yami-1.3.1
## Client ## Client
- フォロー/フォロワー/アナウンス/みつける/Play/ギャラリー/チャンネル/TL/ユーザー/ノートのページをログイン必須に - フォロー/フォロワー/アナウンス/みつける/Play/ギャラリー/チャンネル/TL/ユーザー/ノートのページをログイン必須に
@ -28,6 +29,8 @@
### Server ### Server
- `notes/show`, `users/notes`の認証を不要に(revert?) - `notes/show`, `users/notes`の認証を不要に(revert?)
=======
>>>>>>> 12828a4aa2 (Merge pull request #35 from lqvp/master)
## 2024.9.0-yami-1.2.5 ## 2024.9.0-yami-1.2.5
### Feat ### Feat
- フォロー解除時にも通知するように - フォロー解除時にも通知するように

View File

@ -68,6 +68,7 @@ loadMore: "Load more"
showMore: "Show more" showMore: "Show more"
showLess: "Close" showLess: "Close"
youGotNewFollower: "followed you" youGotNewFollower: "followed you"
youGotUnFollower: "unfollowed you"
receiveFollowRequest: "Follow request received" receiveFollowRequest: "Follow request received"
followRequestAccepted: "Follow request accepted" followRequestAccepted: "Follow request accepted"
mention: "Mention" mention: "Mention"
@ -1884,6 +1885,8 @@ _gallery:
_email: _email:
_follow: _follow:
title: "You've got a new follower" title: "You've got a new follower"
_unfollow:
title: "You have been unfollowed"
_receiveFollowRequest: _receiveFollowRequest:
title: "You've received a follow request" title: "You've received a follow request"
_plugin: _plugin:
@ -2440,6 +2443,7 @@ _notification:
youGotQuote: "{name} quoted you" youGotQuote: "{name} quoted you"
youRenoted: "Boost from {name}" youRenoted: "Boost from {name}"
youWereFollowed: "followed you" youWereFollowed: "followed you"
youWereUnFollower: "unfollowed you"
youReceivedFollowRequest: "You've received a follow request" youReceivedFollowRequest: "You've received a follow request"
yourFollowRequestAccepted: "Your follow request was accepted" yourFollowRequestAccepted: "Your follow request was accepted"
pollEnded: "Poll results have become available" pollEnded: "Poll results have become available"
@ -2456,12 +2460,14 @@ _notification:
likedBySomeUsers: "{n} users liked your note" likedBySomeUsers: "{n} users liked your note"
renotedBySomeUsers: "Boosted by {n} users" renotedBySomeUsers: "Boosted by {n} users"
followedBySomeUsers: "Followed by {n} users" followedBySomeUsers: "Followed by {n} users"
unfollowerBySomeUsers: "UnFollowed by {n} users"
flushNotification: "Clear notifications" flushNotification: "Clear notifications"
edited: "Note got edited" edited: "Note got edited"
_types: _types:
all: "All" all: "All"
note: "New notes" note: "New notes"
follow: "New followers" follow: "New followers"
unfollow: "Unfollowers"
mention: "Mentions" mention: "Mentions"
reply: "Replies" reply: "Replies"
renote: "Boosts" renote: "Boosts"

22
locales/index.d.ts vendored
View File

@ -288,6 +288,10 @@ export interface Locale extends ILocale {
* *
*/ */
"youGotNewFollower": string; "youGotNewFollower": string;
/**
*
*/
"youGotUnFollower": string;
/** /**
* *
*/ */
@ -7340,6 +7344,12 @@ export interface Locale extends ILocale {
*/ */
"title": string; "title": string;
}; };
"_unfollow" : {
/**
*
*/
"title": string;
}
"_receiveFollowRequest": { "_receiveFollowRequest": {
/** /**
* *
@ -9480,6 +9490,10 @@ export interface Locale extends ILocale {
* *
*/ */
"youWereFollowed": string; "youWereFollowed": string;
/**
*
*/
"youWereUnFollower": string;
/** /**
* *
*/ */
@ -9548,6 +9562,10 @@ export interface Locale extends ILocale {
* {n} * {n}
*/ */
"followedBySomeUsers": ParameterizedString<"n">; "followedBySomeUsers": ParameterizedString<"n">;
/**
* {n}
*/
"unfollowerBySomeUser": ParameterizedString<"n">;
/** /**
* *
*/ */
@ -9565,6 +9583,10 @@ export interface Locale extends ILocale {
* *
*/ */
"follow": string; "follow": string;
/**
*
*/
"unfollow": string;
/** /**
* *
*/ */

View File

@ -68,6 +68,7 @@ loadMore: "もっと見る"
showMore: "もっと見る" showMore: "もっと見る"
showLess: "閉じる" showLess: "閉じる"
youGotNewFollower: "フォローされました" youGotNewFollower: "フォローされました"
youGotUnFollower: "フォロー解除されました"
receiveFollowRequest: "フォローリクエストされました" receiveFollowRequest: "フォローリクエストされました"
followRequestAccepted: "フォローが承認されました" followRequestAccepted: "フォローが承認されました"
mention: "メンション" mention: "メンション"
@ -1907,6 +1908,8 @@ _gallery:
_email: _email:
_follow: _follow:
title: "フォローされました" title: "フォローされました"
_unfollow:
title: "フォロー解除されました"
_receiveFollowRequest: _receiveFollowRequest:
title: "フォローリクエストを受け取りました" title: "フォローリクエストを受け取りました"
@ -2501,6 +2504,7 @@ _notification:
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がBoostしました" youRenoted: "{name}がBoostしました"
youWereFollowed: "フォローされました" youWereFollowed: "フォローされました"
youWereUnFollower: "フォロー解除されました"
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
@ -2518,12 +2522,14 @@ _notification:
likedBySomeUsers: "{n}人がいいねしました" likedBySomeUsers: "{n}人がいいねしました"
renotedBySomeUsers: "{n}人がリノートしました" renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
unfollowerBySomeUsers: "{n}人にフォロー解除されました"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"
_types: _types:
all: "すべて" all: "すべて"
note: "ユーザーの新規投稿" note: "ユーザーの新規投稿"
follow: "フォロー" follow: "フォロー"
unfollow: "フォロー解除"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
renote: "Boost" renote: "Boost"

View File

@ -66,6 +66,7 @@ loadMore: "まだまだあるで!"
showMore: "まだまだあるで!" showMore: "まだまだあるで!"
showLess: "さいなら" showLess: "さいなら"
youGotNewFollower: "フォローされたで" youGotNewFollower: "フォローされたで"
youGotUnFollower: "フォロー解除されたで"
receiveFollowRequest: "フォローリクエストされたで" receiveFollowRequest: "フォローリクエストされたで"
followRequestAccepted: "フォローが承認されたで" followRequestAccepted: "フォローが承認されたで"
mention: "メンション" mention: "メンション"
@ -1806,6 +1807,8 @@ _gallery:
_email: _email:
_follow: _follow:
title: "フォローされたで" title: "フォローされたで"
_unfollow:
title: "フォロー解除されたで"
_receiveFollowRequest: _receiveFollowRequest:
title: "フォローリクエストを受け取ったで" title: "フォローリクエストを受け取ったで"
_plugin: _plugin:
@ -2349,6 +2352,7 @@ _notification:
youGotQuote: "{name}による引用" youGotQuote: "{name}による引用"
youRenoted: "{name}がブーストしたみたいやで" youRenoted: "{name}がブーストしたみたいやで"
youWereFollowed: "フォローされたで" youWereFollowed: "フォローされたで"
youWereUnFollower: "フォロー解除されたで"
youReceivedFollowRequest: "フォロー許可してほしいみたいやな" youReceivedFollowRequest: "フォロー許可してほしいみたいやな"
yourFollowRequestAccepted: "フォローさせてもろたで" yourFollowRequestAccepted: "フォローさせてもろたで"
pollEnded: "アンケートの結果が出たみたいや" pollEnded: "アンケートの結果が出たみたいや"
@ -2365,11 +2369,13 @@ _notification:
likedBySomeUsers: "{n}人がいいねしたで" likedBySomeUsers: "{n}人がいいねしたで"
renotedBySomeUsers: "{n}人がブーストしたで" renotedBySomeUsers: "{n}人がブーストしたで"
followedBySomeUsers: "{n}人にフォローされたで" followedBySomeUsers: "{n}人にフォローされたで"
unfollowerBySomeUser: "{n}人にフォロー解除されたで"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"
_types: _types:
all: "すべて" all: "すべて"
note: "あんたらの新規投稿" note: "あんたらの新規投稿"
follow: "フォロー" follow: "フォロー"
unfollow: "フォロー解除"
mention: "メンション" mention: "メンション"
reply: "リプライ" reply: "リプライ"
renote: "Renote" renote: "Renote"

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View File

@ -179,6 +179,7 @@ export class NotificationService implements OnApplicationShutdown {
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed); this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! })); if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
if (type === 'unfollow') this.emailNotificationUnFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! })); if (type === 'receiveFollowRequest') this.emailNotificationReceiveFollowRequest(notifieeId, await this.usersRepository.findOneByOrFail({ id: notifierId! }));
}, () => { /* aborted, ignore it */ }); }, () => { /* aborted, ignore it */ });
@ -202,6 +203,18 @@ export class NotificationService implements OnApplicationShutdown {
*/ */
} }
@bindThis
private async emailNotificationUnFollow(userId: MiUser['id'], follower: MiUser) {
/*
const userProfile = await UserProfiles.findOneByOrFail({ userId: userId });
if (!userProfile.email || !userProfile.emailNotificationTypes.includes('follow')) return;
const locale = locales[userProfile.lang ?? 'ja-JP'];
const i18n = new I18n(locale);
// TODO: render user information html
sendEmail(userProfile.email, i18n.t('_email._unfollow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
*/
}
@bindThis @bindThis
private async emailNotificationReceiveFollowRequest(userId: MiUser['id'], follower: MiUser) { private async emailNotificationReceiveFollowRequest(userId: MiUser['id'], follower: MiUser) {
/* /*

View File

@ -431,6 +431,24 @@ export class UserFollowingService implements OnModuleInit {
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as MiPartialRemoteUser, followee as MiPartialLocalUser), followee)); const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower as MiPartialRemoteUser, followee as MiPartialLocalUser), followee));
this.queueService.deliver(followee, content, follower.inbox, false); this.queueService.deliver(followee, content, follower.inbox, false);
} }
// UnFollow
if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(follower.id, followee).then(async packed => {
this.globalEventService.publishMainStream(followee.id, 'unfollow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('unfollow'));
for (const webhook of webhooks) {
this.queueService.userWebhookDeliver(webhook, 'unfollow', {
user: packed,
});
}
});
// 通知を作成
this.notificationService.createNotification(followee.id, 'unfollow', {
}, follower.id);
}
} }
@bindThis @bindThis

View File

@ -19,6 +19,11 @@ export type MiNotification = {
id: string; id: string;
createdAt: string; createdAt: string;
notifierId: MiUser['id']; notifierId: MiUser['id'];
} | {
type: 'unfollow';
id: string;
createdAt: string;
notifierId: MiUser['id'];
} | { } | {
type: 'mention'; type: 'mention';
id: string; id: string;

View File

@ -6,6 +6,7 @@
/** /**
* note - 稿 * note - 稿
* follow - * follow -
* unfollow -
* mention - 稿 * mention - 稿
* reply - 稿 * reply - 稿
* renote - 稿Renoteされた * renote - 稿Renoteされた
@ -22,6 +23,7 @@
export const notificationTypes = [ export const notificationTypes = [
'note', 'note',
'follow', 'follow',
'unfollow',
'mention', 'mention',
'reply', 'reply',
'renote', 'renote',

View File

@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div <div
:class="[$style.subIcon, { :class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow', [$style.t_follow]: notification.type === 'follow',
[$style.t_unfollow]: notification.type === 'unfollow',
[$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted', [$style.t_followRequestAccepted]: notification.type === 'followRequestAccepted',
[$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest', [$style.t_receiveFollowRequest]: notification.type === 'receiveFollowRequest',
[$style.t_renote]: notification.type === 'renote', [$style.t_renote]: notification.type === 'renote',
@ -30,6 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
}]" }]"
> <!-- we re-use t_pollEnded for "edited" instead of making an identical style --> > <!-- we re-use t_pollEnded for "edited" instead of making an identical style -->
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i> <i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'unfollow'" class="ti ti-minus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i> <i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i> <i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
<i v-else-if="notification.type === 'renote'" class="ti ti-repeat"></i> <i v-else-if="notification.type === 'renote'" class="ti ti-repeat"></i>
@ -60,7 +62,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'unfollow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
@ -106,6 +109,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
</template> </template>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<template v-else-if="notification.type === 'unfollow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotUnFollower }}</span>
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
</template>
<template v-else-if="notification.type === 'receiveFollowRequest'"> <template v-else-if="notification.type === 'receiveFollowRequest'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands"> <div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
@ -200,6 +207,7 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
overflow-wrap: break-word; overflow-wrap: break-word;
display: flex; display: flex;
contain: content; contain: content;
--eventUnFollow: #f08080;
} }
.head { .head {
@ -272,6 +280,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none; pointer-events: none;
} }
.t_unfollow {
padding: 3px;
background: var(--eventUnFollow);
pointer-events: none;
}
.t_renote { .t_renote {
padding: 3px; padding: 3px;
background: var(--eventRenote); background: var(--eventRenote);

View File

@ -108,6 +108,7 @@ https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
export const notificationTypes = [ export const notificationTypes = [
'note', 'note',
'follow', 'follow',
'unfollow',
'mention', 'mention',
'reply', 'reply',
'renote', 'renote',

View File

@ -36,6 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="emailNotification_follow"> <MkSwitch v-model="emailNotification_follow">
{{ i18n.ts._notification._types.follow }} {{ i18n.ts._notification._types.follow }}
</MkSwitch> </MkSwitch>
<MkSwitch v-model="emailNotification_unfollow">
{{ i18n.ts._notification._types.unfollow }}
</MkSwitch>
<MkSwitch v-model="emailNotification_receiveFollowRequest"> <MkSwitch v-model="emailNotification_receiveFollowRequest">
{{ i18n.ts._notification._types.receiveFollowRequest }} {{ i18n.ts._notification._types.receiveFollowRequest }}
</MkSwitch> </MkSwitch>
@ -85,6 +88,7 @@ const emailNotification_mention = ref($i.emailNotificationTypes.includes('mentio
const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply')); const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply'));
const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote')); const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote'));
const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow')); const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow'));
const emailNotification_unfollow = ref($i.emailNotificationTypes.includes('unfollow'))
const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest')); const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest'));
const saveNotificationSettings = () => { const saveNotificationSettings = () => {
@ -94,12 +98,13 @@ const saveNotificationSettings = () => {
...[emailNotification_reply.value ? 'reply' : null], ...[emailNotification_reply.value ? 'reply' : null],
...[emailNotification_quote.value ? 'quote' : null], ...[emailNotification_quote.value ? 'quote' : null],
...[emailNotification_follow.value ? 'follow' : null], ...[emailNotification_follow.value ? 'follow' : null],
...[emailNotification_unfollow.value ? 'unfollow' : null],
...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null], ...[emailNotification_receiveFollowRequest.value ? 'receiveFollowRequest' : null],
].filter(x => x != null), ].filter(x => x != null),
}); });
}; };
watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_receiveFollowRequest], () => { watch([emailNotification_mention, emailNotification_reply, emailNotification_quote, emailNotification_follow, emailNotification_unfollow,emailNotification_receiveFollowRequest], () => {
saveNotificationSettings(); saveNotificationSettings();
}); });

View File

@ -72,6 +72,25 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
}]; }];
} }
case 'unfollow': {
// フォローが外されたときの処理
const account = await getAccountFromId(data.userId);
if (!account) return null;
const userDetail = await cli.request('users/show', { userId: data.body.userId }, account.token);
return [i18n.ts._notification.youWereUnFollower, {
body: getUserName(data.body.user),
icon: data.body.user.avatarUrl ?? undefined,
badge: iconUrl('user-minus'),
data,
actions: userDetail.isFollowing ? [] : [
{
action: 'follow',
title: i18n.ts._notification._actions.followBack,
},
],
}];
}
case 'mention': case 'mention':
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), { return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
body: data.body.note.text ?? '', body: data.body.note.text ?? '',