enhance: 通知の履歴をリセットできるように (#13335)

* enhance: 通知の履歴をリセットできるように

* Update Changelog

* 通知欄も連動して更新するように

* revert some changes

* Update CHANGELOG.md

* Remove unused part

* fix
This commit is contained in:
かっこかり 2024-02-29 20:03:30 +09:00 committed by GitHub
parent ec18991328
commit 39d6af135f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 139 additions and 1 deletions

View file

@ -18,6 +18,7 @@
- Enhance: サーバーごとにモデレーションノートを残せるように - Enhance: サーバーごとにモデレーションノートを残せるように
- Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加 - Enhance: コンディショナルロールの条件に「マニュアルロールへのアサイン」を追加
- Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加 - Enhance: 通知の受信設定に「フォロー中またはフォロワー」を追加
- Enhance: 通知の履歴をリセットできるように
### Client ### Client
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整

4
locales/index.d.ts vendored
View file

@ -8913,6 +8913,10 @@ export interface Locale extends ILocale {
* {n} * {n}
*/ */
"followedBySomeUsers": ParameterizedString<"n">; "followedBySomeUsers": ParameterizedString<"n">;
/**
*
*/
"flushNotification": string;
"_types": { "_types": {
/** /**
* *

View file

@ -2356,6 +2356,7 @@ _notification:
reactedBySomeUsers: "{n}人がリアクションしました" reactedBySomeUsers: "{n}人がリアクションしました"
renotedBySomeUsers: "{n}人がリノートしました" renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする"
_types: _types:
all: "すべて" all: "すべて"

View file

@ -69,6 +69,7 @@ export interface MainEventTypes {
file: Packed<'DriveFile'>; file: Packed<'DriveFile'>;
}; };
readAllNotifications: undefined; readAllNotifications: undefined;
notificationFlushed: undefined;
unreadNotification: Packed<'Notification'>; unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id']; unreadMention: MiNote['id'];
readAllUnreadMentions: undefined; readAllUnreadMentions: undefined;

View file

@ -214,6 +214,15 @@ export class NotificationService implements OnApplicationShutdown {
*/ */
} }
@bindThis
public async flushAllNotifications(userId: MiUser['id']) {
await Promise.all([
this.redisClient.del(`notificationTimeline:${userId}`),
this.redisClient.del(`latestReadNotification:${userId}`),
]);
this.globalEventService.publishMainStream(userId, 'notificationFlushed');
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.#shutdownController.abort(); this.#shutdownController.abort();

View file

@ -293,6 +293,7 @@ import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_flush from './endpoints/notifications/flush.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pagePush from './endpoints/page-push.js';
@ -664,6 +665,7 @@ const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep
const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default }; const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default };
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default }; const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default }; const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
const $notifications_flush: Provider = { provide: 'ep:notifications/flush', useClass: ep___notifications_flush.default };
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default }; const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default }; const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default }; const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
@ -1039,6 +1041,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_unrenote, $notes_unrenote,
$notes_userListTimeline, $notes_userListTimeline,
$notifications_create, $notifications_create,
$notifications_flush,
$notifications_markAllAsRead, $notifications_markAllAsRead,
$notifications_testNotification, $notifications_testNotification,
$pagePush, $pagePush,
@ -1408,7 +1411,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_unrenote, $notes_unrenote,
$notes_userListTimeline, $notes_userListTimeline,
$notifications_create, $notifications_create,
$notifications_flush,
$notifications_markAllAsRead, $notifications_markAllAsRead,
$notifications_testNotification,
$pagePush, $pagePush,
$pages_create, $pages_create,
$pages_delete, $pages_delete,

View file

@ -293,6 +293,7 @@ import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js'; import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js'; import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js'; import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_flush from './endpoints/notifications/flush.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js'; import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js'; import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js'; import * as ep___pagePush from './endpoints/page-push.js';
@ -662,6 +663,7 @@ const eps = [
['notes/unrenote', ep___notes_unrenote], ['notes/unrenote', ep___notes_unrenote],
['notes/user-list-timeline', ep___notes_userListTimeline], ['notes/user-list-timeline', ep___notes_userListTimeline],
['notifications/create', ep___notifications_create], ['notifications/create', ep___notifications_create],
['notifications/flush', ep___notifications_flush],
['notifications/mark-all-as-read', ep___notifications_markAllAsRead], ['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
['notifications/test-notification', ep___notifications_testNotification], ['notifications/test-notification', ep___notifications_testNotification],
['page-push', ep___pagePush], ['page-push', ep___pagePush],

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NotificationService } from '@/core/NotificationService.js';
export const meta = {
tags: ['notifications', 'account'],
requireCredential: true,
kind: 'write:notifications',
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
this.notificationService.flushAllNotifications(me.id);
});
}
}

View file

@ -35,6 +35,7 @@ import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import * as Misskey from 'misskey-js';
const props = defineProps<{ const props = defineProps<{
excludeTypes?: typeof notificationTypes[number][]; excludeTypes?: typeof notificationTypes[number][];
@ -75,17 +76,19 @@ function reload() {
}); });
} }
let connection; let connection: Misskey.ChannelConnection<Misskey.Channels['main']>;
onMounted(() => { onMounted(() => {
connection = useStream().useChannel('main'); connection = useStream().useChannel('main');
connection.on('notification', onNotification); connection.on('notification', onNotification);
connection.on('notificationFlushed', reload);
}); });
onActivated(() => { onActivated(() => {
pagingComponent.value?.reload(); pagingComponent.value?.reload();
connection = useStream().useChannel('main'); connection = useStream().useChannel('main');
connection.on('notification', onNotification); connection.on('notification', onNotification);
connection.on('notificationFlushed', reload);
}); });
onUnmounted(() => { onUnmounted(() => {

View file

@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection> <FormSection>
<div class="_gaps_m"> <div class="_gaps_m">
<FormLink @click="testNotification">{{ i18n.ts._notification.sendTestNotification }}</FormLink> <FormLink @click="testNotification">{{ i18n.ts._notification.sendTestNotification }}</FormLink>
<FormLink @click="flushNotification">{{ i18n.ts._notification.flushNotification }}</FormLink>
</div> </div>
</FormSection> </FormSection>
<FormSection> <FormSection>
@ -114,6 +115,17 @@ function testNotification(): void {
misskeyApi('notifications/test-notification'); misskeyApi('notifications/test-notification');
} }
async function flushNotification() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.resetAreYouSure,
});
if (canceled) return;
os.apiWithDialog('notifications/flush');
}
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View file

@ -530,6 +530,7 @@ export type Channels = {
unreadNotification: (payload: Notification_2) => void; unreadNotification: (payload: Notification_2) => void;
unreadMention: (payload: Note['id']) => void; unreadMention: (payload: Note['id']) => void;
readAllUnreadMentions: () => void; readAllUnreadMentions: () => void;
notificationFlushed: () => void;
unreadSpecifiedNote: (payload: Note['id']) => void; unreadSpecifiedNote: (payload: Note['id']) => void;
readAllUnreadSpecifiedNotes: () => void; readAllUnreadSpecifiedNotes: () => void;
readAllAntennas: () => void; readAllAntennas: () => void;

View file

@ -3195,6 +3195,17 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notifications*
*/
request<E extends 'notifications/flush', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/** /**
* No description provided. * No description provided.
* *

View file

@ -841,6 +841,7 @@ export type Endpoints = {
'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse }; 'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse };
'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse }; 'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse };
'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse }; 'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse };
'notifications/flush': { req: EmptyRequest; res: EmptyResponse };
'notifications/mark-all-as-read': { req: EmptyRequest; res: EmptyResponse }; 'notifications/mark-all-as-read': { req: EmptyRequest; res: EmptyResponse };
'notifications/test-notification': { req: EmptyRequest; res: EmptyResponse }; 'notifications/test-notification': { req: EmptyRequest; res: EmptyResponse };
'page-push': { req: PagePushRequest; res: EmptyResponse }; 'page-push': { req: PagePushRequest; res: EmptyResponse };

View file

@ -2770,6 +2770,15 @@ export type paths = {
*/ */
post: operations['notifications/create']; post: operations['notifications/create'];
}; };
'/notifications/flush': {
/**
* notifications/flush
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notifications*
*/
post: operations['notifications/flush'];
};
'/notifications/mark-all-as-read': { '/notifications/mark-all-as-read': {
/** /**
* notifications/mark-all-as-read * notifications/mark-all-as-read
@ -22056,6 +22065,50 @@ export type operations = {
}; };
}; };
}; };
/**
* notifications/flush
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notifications*
*/
'notifications/flush': {
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/** /**
* notifications/mark-all-as-read * notifications/mark-all-as-read
* @description No description provided. * @description No description provided.

View file

@ -40,6 +40,7 @@ export type Channels = {
unreadNotification: (payload: Notification) => void; unreadNotification: (payload: Notification) => void;
unreadMention: (payload: Note['id']) => void; unreadMention: (payload: Note['id']) => void;
readAllUnreadMentions: () => void; readAllUnreadMentions: () => void;
notificationFlushed: () => void;
unreadSpecifiedNote: (payload: Note['id']) => void; unreadSpecifiedNote: (payload: Note['id']) => void;
readAllUnreadSpecifiedNotes: () => void; readAllUnreadSpecifiedNotes: () => void;
readAllAntennas: () => void; readAllAntennas: () => void;