@@ -264,6 +270,20 @@ watch(syncDeviceDarkMode, () => {
}
});
+const themesSyncEnabled = ref(prefer.isSyncEnabled('themes'));
+
+function changeThemesSyncEnabled(value: boolean) {
+ if (value) {
+ prefer.enableSync('themes').then((res) => {
+ if (res == null) return;
+ if (res.enabled) themesSyncEnabled.value = true;
+ });
+ } else {
+ prefer.disableSync('themes');
+ themesSyncEnabled.value = false;
+ }
+}
+
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index 08ab0d8811..7f800d2b70 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -37,6 +37,11 @@ export const searchIndexes: SearchIndexItem[] = [
label: i18n.ts.themeForDarkMode,
keywords: ['dark', 'theme'],
},
+ {
+ id: 'jwW5HULqA',
+ label: i18n.ts._settings.enableSyncThemesBetweenDevices,
+ keywords: ['sync', 'themes', 'devices'],
+ },
],
label: i18n.ts.theme,
keywords: ['theme'],
From 3ec5bf114b06b620074c307072824d3e078b4946 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 2 Apr 2025 10:25:53 +0900
Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/frontend/src/pages/settings/deck.vue | 2 +-
packages/frontend/src/pages/settings/emoji-palette.vue | 2 +-
packages/frontend/src/pages/settings/theme.vue | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index f4bce8a5dd..39055268d4 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}
+ {{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
index 398228e226..f86c40412a 100644
--- a/packages/frontend/src/pages/settings/emoji-palette.vue
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}
+ {{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index adc93ff7a7..ad641d37db 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -183,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts._settings.enableSyncThemesBetweenDevices }}
+ {{ i18n.ts._settings.enableSyncThemesBetweenDevices }}
From 55d835ad51dd4e114d367b3711ce0025a15fe26f Mon Sep 17 00:00:00 2001
From: anatawa12
Date: Wed, 2 Apr 2025 10:37:16 +0900
Subject: [PATCH 03/12] =?UTF-8?q?Fix:=20=E9=80=9A=E7=9F=A5=E3=81=AE?=
=?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=83=8D=E3=83=BC=E3=82=B7=E3=83=A7?=
=?UTF-8?q?=E3=83=B3=E3=81=A7=EF=BC=92=E3=81=A4=E4=BB=A5=E4=B8=8A=E8=AA=AD?=
=?UTF-8?q?=E3=81=BF=E8=BE=BC=E3=82=81=E3=81=AA=E3=81=8F=E3=81=AA=E3=82=8B?=
=?UTF-8?q?=E3=81=93=E3=81=A8=E3=81=8C=E3=81=82=E3=82=8B=E5=95=8F=E9=A1=8C?=
=?UTF-8?q?=20(#15277)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: notifications-groupedのinclude/exclude typesに:groupedを指定できてしまう問題
* refactor: 通知の取得処理を Notification Service に移動
* feat: add function to parse additional part of id
* fix: 通知のページネーションが正しく動かない問題
Redisにのページネーションで使用する時間及びidとRedis上のものが混同されていたので、Misskeyが生成するものに寄せました。
* pnpm run build-misskey-js-with-types
* chore: XADDをretryするように
* fix: notifications-groupedでxrevrangeしているのを消し忘れていた
---
packages/backend/src/core/IdService.ts | 26 +++-
.../backend/src/core/NotificationService.ts | 121 +++++++++++++++---
packages/backend/src/misc/bigint.ts | 40 ++++++
packages/backend/src/misc/id/aid.ts | 7 +
packages/backend/src/misc/id/aidx.ts | 8 ++
packages/backend/src/misc/id/meid.ts | 9 ++
packages/backend/src/misc/id/meidg.ts | 9 ++
packages/backend/src/misc/id/object-id.ts | 9 ++
packages/backend/src/misc/id/ulid.ts | 20 ++-
.../api/endpoints/i/notifications-grouped.ts | 38 +++---
.../server/api/endpoints/i/notifications.ts | 53 +-------
packages/misskey-js/src/autogen/types.ts | 4 +-
12 files changed, 248 insertions(+), 96 deletions(-)
create mode 100644 packages/backend/src/misc/bigint.ts
diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts
index 10df6ef266..223a8de678 100644
--- a/packages/backend/src/core/IdService.ts
+++ b/packages/backend/src/core/IdService.ts
@@ -7,13 +7,13 @@ import { Inject, Injectable } from '@nestjs/common';
import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { genAid, isSafeAidT, parseAid } from '@/misc/id/aid.js';
-import { genAidx, isSafeAidxT, parseAidx } from '@/misc/id/aidx.js';
-import { genMeid, isSafeMeidT, parseMeid } from '@/misc/id/meid.js';
-import { genMeidg, isSafeMeidgT, parseMeidg } from '@/misc/id/meidg.js';
-import { genObjectId, isSafeObjectIdT, parseObjectId } from '@/misc/id/object-id.js';
+import { genAid, isSafeAidT, parseAid, parseAidFull } from '@/misc/id/aid.js';
+import { genAidx, isSafeAidxT, parseAidx, parseAidxFull } from '@/misc/id/aidx.js';
+import { genMeid, isSafeMeidT, parseMeid, parseMeidFull } from '@/misc/id/meid.js';
+import { genMeidg, isSafeMeidgT, parseMeidg, parseMeidgFull } from '@/misc/id/meidg.js';
+import { genObjectId, isSafeObjectIdT, parseObjectId, parseObjectIdFull } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js';
-import { parseUlid } from '@/misc/id/ulid.js';
+import { parseUlid, parseUlidFull } from '@/misc/id/ulid.js';
@Injectable()
export class IdService {
@@ -70,4 +70,18 @@ export class IdService {
default: throw new Error('unrecognized id generation method');
}
}
+
+ // Note: additional is at most 64 bits
+ @bindThis
+ public parseFull(id: string): { date: number; additional: bigint; } {
+ switch (this.method) {
+ case 'aid': return parseAidFull(id);
+ case 'aidx': return parseAidxFull(id);
+ case 'objectid': return parseObjectIdFull(id);
+ case 'meid': return parseMeidFull(id);
+ case 'meidg': return parseMeidgFull(id);
+ case 'ulid': return parseUlidFull(id);
+ default: throw new Error('unrecognized id generation method');
+ }
+ }
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index 68ad92f396..eeade4569b 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -7,6 +7,7 @@ import { setTimeout } from 'node:timers/promises';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
+import { ReplyError } from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
@@ -19,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { UserListService } from '@/core/UserListService.js';
-import type { FilterUnionByProperty } from '@/types.js';
+import { FilterUnionByProperty, groupedNotificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { trackPromise } from '@/misc/promise-tracker.js';
@Injectable()
@@ -145,21 +146,36 @@ export class NotificationService implements OnApplicationShutdown {
}
}
- const notification = {
- id: this.idService.gen(),
- createdAt: new Date(),
- type: type,
- ...(notifierId ? {
- notifierId,
- } : {}),
- ...data,
- } as any as FilterUnionByProperty;
+ const createdAt = new Date();
+ let notification: FilterUnionByProperty;
+ let redisId: string;
- const redisIdPromise = this.redisClient.xadd(
- `notificationTimeline:${notifieeId}`,
- 'MAXLEN', '~', this.config.perUserNotificationsMaxCount.toString(),
- '*',
- 'data', JSON.stringify(notification));
+ do {
+ notification = {
+ id: this.idService.gen(),
+ createdAt,
+ type: type,
+ ...(notifierId ? {
+ notifierId,
+ } : {}),
+ ...data,
+ } as unknown as FilterUnionByProperty;
+
+ try {
+ redisId = (await this.redisClient.xadd(
+ `notificationTimeline:${notifieeId}`,
+ 'MAXLEN', '~', this.config.perUserNotificationsMaxCount.toString(),
+ this.toXListId(notification.id),
+ 'data', JSON.stringify(notification)))!;
+ } catch (e) {
+ // The ID specified in XADD is equal or smaller than the target stream top item で失敗することがあるのでリトライ
+ if (e instanceof ReplyError) continue;
+ throw e;
+ }
+
+ break;
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ } while (true);
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
@@ -173,7 +189,7 @@ export class NotificationService implements OnApplicationShutdown {
const interval = notification.type === 'test' ? 0 : 2000;
setTimeout(interval, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
- if (latestReadNotificationId && (latestReadNotificationId >= (await redisIdPromise)!)) return;
+ if (latestReadNotificationId && (latestReadNotificationId >= redisId)) return;
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
@@ -228,6 +244,79 @@ export class NotificationService implements OnApplicationShutdown {
this.#shutdownController.abort();
}
+ private toXListId(id: string): string {
+ const { date, additional } = this.idService.parseFull(id);
+ return date.toString() + '-' + additional.toString();
+ }
+
+ @bindThis
+ public async getNotifications(
+ userId: MiUser['id'],
+ {
+ sinceId,
+ untilId,
+ limit = 20,
+ includeTypes,
+ excludeTypes,
+ }: {
+ sinceId?: string,
+ untilId?: string,
+ limit?: number,
+ // any extra types are allowed, those are no-op
+ includeTypes?: (MiNotification['type'] | string)[],
+ excludeTypes?: (MiNotification['type'] | string)[],
+ },
+ ): Promise {
+ let sinceTime = sinceId ? this.toXListId(sinceId) : null;
+ let untilTime = untilId ? this.toXListId(untilId) : null;
+
+ let notifications: MiNotification[];
+ for (;;) {
+ let notificationsRes: [id: string, fields: string[]][];
+
+ // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
+ if (sinceTime && !untilTime) {
+ notificationsRes = await this.redisClient.xrange(
+ `notificationTimeline:${userId}`,
+ '(' + sinceTime,
+ '+',
+ 'COUNT', limit);
+ } else {
+ notificationsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${userId}`,
+ untilTime ? '(' + untilTime : '+',
+ sinceTime ? '(' + sinceTime : '-',
+ 'COUNT', limit);
+ }
+
+ if (notificationsRes.length === 0) {
+ return [];
+ }
+
+ notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
+
+ if (includeTypes && includeTypes.length > 0) {
+ notifications = notifications.filter(notification => includeTypes.includes(notification.type));
+ } else if (excludeTypes && excludeTypes.length > 0) {
+ notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
+ }
+
+ if (notifications.length !== 0) {
+ // 通知が1件以上ある場合は返す
+ break;
+ }
+
+ // フィルタしたことで通知が0件になった場合、次のページを取得する
+ if (sinceId && !untilId) {
+ sinceTime = notificationsRes[notificationsRes.length - 1][0];
+ } else {
+ untilTime = notificationsRes[notificationsRes.length - 1][0];
+ }
+ }
+
+ return notifications;
+ }
+
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
diff --git a/packages/backend/src/misc/bigint.ts b/packages/backend/src/misc/bigint.ts
new file mode 100644
index 0000000000..efa1527ec9
--- /dev/null
+++ b/packages/backend/src/misc/bigint.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+function parseBigIntChunked(str: string, base: number, chunkSize: number, powerOfChunkSize: bigint): bigint {
+ const chunks = [];
+ while (str.length > 0) {
+ chunks.unshift(str.slice(-chunkSize));
+ str = str.slice(0, -chunkSize);
+ }
+ let result = 0n;
+ for (const chunk of chunks) {
+ result *= powerOfChunkSize;
+ const int = parseInt(chunk, base);
+ if (Number.isNaN(int)) {
+ throw new Error('Invalid base36 string');
+ }
+ result += BigInt(int);
+ }
+ return result;
+}
+
+export function parseBigInt36(str: string): bigint {
+ // log_36(Number.MAX_SAFE_INTEGER) => 10.251599391715352
+ // so we process 10 chars at once
+ return parseBigIntChunked(str, 36, 10, 36n ** 10n);
+}
+
+export function parseBigInt16(str: string): bigint {
+ // log_16(Number.MAX_SAFE_INTEGER) => 13.25
+ // so we process 13 chars at once
+ return parseBigIntChunked(str, 16, 13, 16n ** 13n);
+}
+
+export function parseBigInt32(str: string): bigint {
+ // log_32(Number.MAX_SAFE_INTEGER) => 10.6
+ // so we process 10 chars at once
+ return parseBigIntChunked(str, 32, 10, 32n ** 10n);
+}
diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts
index 60ba788e44..c0e8478db5 100644
--- a/packages/backend/src/misc/id/aid.ts
+++ b/packages/backend/src/misc/id/aid.ts
@@ -7,6 +7,7 @@
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列]
import * as crypto from 'node:crypto';
+import { parseBigInt36 } from '@/misc/bigint.js';
export const aidRegExp = /^[0-9a-z]{10}$/;
@@ -35,6 +36,12 @@ export function parseAid(id: string): { date: Date; } {
return { date: new Date(time) };
}
+export function parseAidFull(id: string): { date: number; additional: bigint; } {
+ const date = parseInt(id.slice(0, 8), 36) + TIME2000;
+ const additional = parseBigInt36(id.slice(8, 10));
+ return { date, additional };
+}
+
export function isSafeAidT(t: number): boolean {
return t > TIME2000;
}
diff --git a/packages/backend/src/misc/id/aidx.ts b/packages/backend/src/misc/id/aidx.ts
index 1b087e70af..006673a6d0 100644
--- a/packages/backend/src/misc/id/aidx.ts
+++ b/packages/backend/src/misc/id/aidx.ts
@@ -9,6 +9,7 @@
// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24
import { customAlphabet } from 'nanoid';
+import { parseBigInt36 } from '@/misc/bigint.js';
export const aidxRegExp = /^[0-9a-z]{16}$/;
@@ -16,6 +17,7 @@ const TIME2000 = 946684800000;
const TIME_LENGTH = 8;
const NODE_LENGTH = 4;
const NOISE_LENGTH = 4;
+const AIDX_LENGTH = TIME_LENGTH + NODE_LENGTH + NOISE_LENGTH;
const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)();
let counter = 0;
@@ -42,6 +44,12 @@ export function parseAidx(id: string): { date: Date; } {
return { date: new Date(time) };
}
+export function parseAidxFull(id: string): { date: number; additional: bigint; } {
+ const date = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
+ const additional = parseBigInt36(id.slice(TIME_LENGTH, AIDX_LENGTH));
+ return { date, additional };
+}
+
export function isSafeAidxT(t: number): boolean {
return t > TIME2000;
}
diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts
index dfab48a369..563e07ed8f 100644
--- a/packages/backend/src/misc/id/meid.ts
+++ b/packages/backend/src/misc/id/meid.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { parseBigInt16 } from '@/misc/bigint.js';
+
const CHARS = '0123456789abcdef';
// same as object-id
@@ -39,6 +41,13 @@ export function parseMeid(id: string): { date: Date; } {
};
}
+export function parseMeidFull(id: string): { date: number; additional: bigint; } {
+ return {
+ date: parseInt(id.slice(0, 12), 16) - 0x800000000000,
+ additional: parseBigInt16(id.slice(12, 24)),
+ };
+}
+
export function isSafeMeidT(t: number): boolean {
return t > 0;
}
diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts
index b9c0cc3dda..b825807114 100644
--- a/packages/backend/src/misc/id/meidg.ts
+++ b/packages/backend/src/misc/id/meidg.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { parseBigInt16 } from '@/misc/bigint.js';
+
const CHARS = '0123456789abcdef';
// 4bit Fixed hex value 'g'
@@ -39,6 +41,13 @@ export function parseMeidg(id: string): { date: Date; } {
};
}
+export function parseMeidgFull(id: string): { date: number; additional: bigint; } {
+ return {
+ date: parseInt(id.slice(1, 12), 16),
+ additional: parseBigInt16(id.slice(12, 24)),
+ };
+}
+
export function isSafeMeidgT(t: number): boolean {
return t > 0;
}
diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts
index 243f92bbac..68409c7a61 100644
--- a/packages/backend/src/misc/id/object-id.ts
+++ b/packages/backend/src/misc/id/object-id.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { parseBigInt16 } from '@/misc/bigint.js';
+
const CHARS = '0123456789abcdef';
// same as meid
@@ -39,6 +41,13 @@ export function parseObjectId(id: string): { date: Date; } {
};
}
+export function parseObjectIdFull(id: string): { date: number; additional: bigint; } {
+ return {
+ date: parseInt(id.slice(0, 8), 16) * 1000,
+ additional: parseBigInt16(id.slice(8, 24)),
+ };
+}
+
export function isSafeObjectIdT(t: number): boolean {
return t > 0;
}
diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts
index fc3654d6d2..8b81702d19 100644
--- a/packages/backend/src/misc/id/ulid.ts
+++ b/packages/backend/src/misc/id/ulid.ts
@@ -5,15 +5,27 @@
// Crockford's Base32
// https://github.com/ulid/spec#encoding
+import { parseBigInt32 } from '@/misc/bigint.js';
+
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/;
-export function parseUlid(id: string): { date: Date; } {
- const timestamp = id.slice(0, 10);
+function parseBase32(timestamp: string) {
let time = 0;
- for (let i = 0; i < 10; i++) {
+ for (let i = 0; i < timestamp.length; i++) {
time = time * 32 + CHARS.indexOf(timestamp[i]);
}
- return { date: new Date(time) };
+ return time;
+}
+
+export function parseUlid(id: string): { date: Date; } {
+ return { date: new Date(parseBase32(id.slice(0, 10))) };
+}
+
+export function parseUlidFull(id: string): { date: number; additional: bigint; } {
+ return {
+ date: parseBase32(id.slice(0, 10)),
+ additional: parseBigInt32(id.slice(10, 26)),
+ };
}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
index 88d7f51c26..b9c41b057d 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications-grouped.ts
@@ -7,7 +7,12 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
-import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js';
+import {
+ obsoleteNotificationTypes,
+ groupedNotificationTypes,
+ FilterUnionByProperty,
+ notificationTypes,
+} from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { NotificationService } from '@/core/NotificationService.js';
@@ -47,10 +52,10 @@ export const paramDef = {
markAsRead: { type: 'boolean', default: true },
// 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
- type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
excludeTypes: { type: 'array', items: {
- type: 'string', enum: [...groupedNotificationTypes, ...obsoleteNotificationTypes],
+ type: 'string', enum: [...notificationTypes, ...obsoleteNotificationTypes],
} },
},
required: [],
@@ -74,31 +79,20 @@ export default class extends Endpoint { // eslint-
return [];
}
// excludeTypes に全指定されている場合はクエリしない
- if (groupedNotificationTypes.every(type => ps.excludeTypes?.includes(type))) {
+ if (notificationTypes.every(type => ps.excludeTypes?.includes(type))) {
return [];
}
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof groupedNotificationTypes[number][];
- const limit = (ps.limit + EXTRA_LIMIT) + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- const notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
- ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
- 'COUNT', limit);
-
- if (notificationsRes.length === 0) {
- return [];
- }
-
- let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
-
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
+ const notifications = await this.notificationService.getNotifications(me.id, {
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ limit: ps.limit,
+ includeTypes,
+ excludeTypes,
+ });
if (notifications.length === 0) {
return [];
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index be8d0cfb34..f5a48b2f69 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -82,52 +82,13 @@ export default class extends Endpoint { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
- let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
-
- let notifications: MiNotification[];
- for (;;) {
- let notificationsRes: [id: string, fields: string[]][];
-
- // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
- if (sinceTime && !untilTime) {
- notificationsRes = await this.redisClient.xrange(
- `notificationTimeline:${me.id}`,
- '(' + sinceTime,
- '+',
- 'COUNT', ps.limit);
- } else {
- notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- untilTime ? '(' + untilTime : '+',
- sinceTime ? '(' + sinceTime : '-',
- 'COUNT', ps.limit);
- }
-
- if (notificationsRes.length === 0) {
- return [];
- }
-
- notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
-
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
-
- if (notifications.length !== 0) {
- // 通知が1件以上ある場合は返す
- break;
- }
-
- // フィルタしたことで通知が0件になった場合、次のページを取得する
- if (ps.sinceId && !ps.untilId) {
- sinceTime = notificationsRes[notificationsRes.length - 1][0];
- } else {
- untilTime = notificationsRes[notificationsRes.length - 1][0];
- }
- }
+ const notifications = await this.notificationService.getNotifications(me.id, {
+ sinceId: ps.sinceId,
+ untilId: ps.untilId,
+ limit: ps.limit,
+ includeTypes,
+ excludeTypes,
+ });
// Mark all as read
if (ps.markAsRead) {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 57c4824424..d46df2423a 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -21732,8 +21732,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
};
};
};
From d44fd87b69aab9569bd80b34f567bba67eee2fff Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 2 Apr 2025 10:37:48 +0900
Subject: [PATCH 04/12] Update CHANGELOG.md
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bc4636120..4dd40a331e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
+- Fix: 通知のページネーションで2つ以上読み込めなくなることがある問題を修正
### Client
- Feat: 設定の管理が強化されました
From 2e0c80bc21f68a3b61dfb50bfd9e3d2fe332fb9d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Wed, 2 Apr 2025 01:47:33 +0000
Subject: [PATCH 05/12] Bump version to 2025.4.0-beta.1
---
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 633a0a0293..2611ddb695 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2025.4.0-beta.0",
+ "version": "2025.4.0-beta.1",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index ed73416e45..33a7070cc6 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.4.0-beta.0",
+ "version": "2025.4.0-beta.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
From 102578712bbed1d4588bf5c2b1996a4655d16de8 Mon Sep 17 00:00:00 2001
From: anatawa12
Date: Wed, 2 Apr 2025 10:48:57 +0900
Subject: [PATCH 06/12] =?UTF-8?q?fix:=20iPad=E3=81=A7deck=20ui=E3=81=A7?=
=?UTF-8?q?=E3=83=9E=E3=82=A6=E3=82=B9=E3=83=9B=E3=82=A4=E3=83=BC=E3=83=AB?=
=?UTF-8?q?=E3=81=A7=E3=82=B9=E3=82=AF=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=A7?=
=?UTF-8?q?=E3=81=8D=E3=81=AA=E3=81=84=20(#15244)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: ipadでdeck uiでスクロールできない
* docs(changelog): iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正
* chore: remove pointermove listener
* lint: use window.document
* chore: use passive pointerdown event
---
CHANGELOG.md | 1 +
packages/frontend/src/ui/deck.vue | 18 ++++++++++++++----
packages/frontend/src/ui/deck/column.vue | 2 +-
3 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dd40a331e..9a2da274e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -57,6 +57,7 @@
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます
+- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正
### Server
- Enhance 全体的なパフォーマンス向上
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index ef4a6fc03c..bf39c07229 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -12,15 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
+
+
{
isMobile.value = window.innerWidth <= 500;
});
-const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet';
+// ポインターイベント非対応用に初期値はUAから出す
+const snapScroll = ref(deviceKind === 'smartphone' || deviceKind === 'tablet');
const withWallpaper = prefer.s['deck.wallpaper'] != null;
const drawerMenuShowing = ref(false);
const gap = prefer.r['deck.columnGap'];
@@ -219,7 +220,16 @@ const onContextmenu = (ev) => {
}], ev);
};
+// タッチでスクロールしてるときはスナップスクロールを有効にする
+function pointerEvent(ev: PointerEvent) {
+ snapScroll.value = ev.pointerType === 'touch';
+}
+
+window.document.addEventListener('pointerdown', pointerEvent, { passive: true });
+
function onWheel(ev: WheelEvent) {
+ // WheelEvent はマウスからしか発火しないのでスナップスクロールは無効化する
+ snapScroll.value = false;
if (ev.deltaX === 0 && columnsEl.value != null) {
columnsEl.value.scrollLeft += ev.deltaY;
}
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 497a04610e..3d359b05c2 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@dragstart="onDragstart"
@dragend="onDragend"
@contextmenu.prevent.stop="onContextmenu"
- @wheel="emit('headerWheel', $event)"
+ @wheel.passive="emit('headerWheel', $event)"
>