summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-04-02 22:29:14 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-04-02 22:29:14 -0400
commitdab9b518e48f04833708a1704e331fd262f575bd (patch)
treeca4dfba2efbe5d550c7d19474d4d91850cb6ccd2
parentfix vue warning about render functions (diff)
parent🎨 (diff)
downloadsharkey-dab9b518e48f04833708a1704e331fd262f575bd.tar.gz
sharkey-dab9b518e48f04833708a1704e331fd262f575bd.tar.bz2
sharkey-dab9b518e48f04833708a1704e331fd262f575bd.zip
merge from misskey-develop
-rw-r--r--.config/cypress-devcontainer.yml5
-rw-r--r--.config/docker_example.yml5
-rw-r--r--.config/example.yml5
-rw-r--r--.devcontainer/devcontainer.yml5
-rw-r--r--CHANGELOG.md6
-rw-r--r--chart/files/default.yml5
-rw-r--r--locales/index.d.ts12
-rw-r--r--locales/ja-JP.yml3
-rw-r--r--package.json2
-rw-r--r--packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js16
-rw-r--r--packages/backend/package.json1
-rw-r--r--packages/backend/src/config.ts17
-rw-r--r--packages/backend/src/core/AntennaService.ts2
-rw-r--r--packages/backend/src/core/IdService.ts26
-rw-r--r--packages/backend/src/core/NoteCreateService.ts5
-rw-r--r--packages/backend/src/core/NotificationService.ts121
-rw-r--r--packages/backend/src/core/entities/AntennaEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts1
-rw-r--r--packages/backend/src/misc/bigint.ts40
-rw-r--r--packages/backend/src/misc/id/aid.ts7
-rw-r--r--packages/backend/src/misc/id/aidx.ts8
-rw-r--r--packages/backend/src/misc/id/meid.ts9
-rw-r--r--packages/backend/src/misc/id/meidg.ts9
-rw-r--r--packages/backend/src/misc/id/object-id.ts9
-rw-r--r--packages/backend/src/misc/id/ulid.ts20
-rw-r--r--packages/backend/src/models/Antenna.ts5
-rw-r--r--packages/backend/src/models/json-schema/antenna.ts5
-rw-r--r--packages/backend/src/models/json-schema/meta.ts32
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/update.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications-grouped.ts38
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts53
-rw-r--r--packages/backend/test/e2e/antennas.ts39
-rw-r--r--packages/frontend-shared/themes/d-astro.json56
-rw-r--r--packages/frontend-shared/themes/d-botanical.json53
-rw-r--r--packages/frontend-shared/themes/d-persimmon.json53
-rw-r--r--packages/frontend/package.json1
-rw-r--r--packages/frontend/src/boot/common.ts37
-rw-r--r--packages/frontend/src/components/MkAntennaEditor.vue4
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.vue1
-rw-r--r--packages/frontend/src/pages/clip.vue2
-rw-r--r--packages/frontend/src/pages/settings/deck.vue2
-rw-r--r--packages/frontend/src/pages/settings/emoji-palette.vue2
-rw-r--r--packages/frontend/src/pages/settings/theme.vue20
-rw-r--r--packages/frontend/src/pages/tag.vue2
-rw-r--r--packages/frontend/src/ui/deck.vue18
-rw-r--r--packages/frontend/src/ui/deck/column.vue2
-rw-r--r--packages/frontend/src/utility/autogen/settings-search-index.ts5
-rw-r--r--packages/frontend/src/utility/get-note-menu.ts4
-rw-r--r--packages/frontend/src/utility/get-user-menu.ts2
-rw-r--r--packages/misskey-js/package.json2
-rw-r--r--packages/misskey-js/src/autogen/types.ts23
-rw-r--r--pnpm-lock.yaml74
54 files changed, 614 insertions, 118 deletions
diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
index cf3786120d..fc58fbab72 100644
--- a/.config/cypress-devcontainer.yml
+++ b/.config/cypress-devcontainer.yml
@@ -197,6 +197,11 @@ id: 'aidx'
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
+# vueIntegration:
+# tracingOptions:
+# trackComponents: true
+# browserTracingIntegration:
+# replayIntegration:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index 1992c87f3d..f9666307de 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -244,6 +244,11 @@ id: 'aidx'
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
+# vueIntegration:
+# tracingOptions:
+# trackComponents: true
+# browserTracingIntegration:
+# replayIntegration:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
diff --git a/.config/example.yml b/.config/example.yml
index dd24981c1f..a4c5165428 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -284,6 +284,11 @@ id: 'aidx'
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
+# vueIntegration:
+# tracingOptions:
+# trackComponents: true
+# browserTracingIntegration:
+# replayIntegration:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index 3eb4fc2879..6d904e87b9 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -152,6 +152,11 @@ id: 'aidx'
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
+# vueIntegration:
+# tracingOptions:
+# trackComponents: true
+# browserTracingIntegration:
+# replayIntegration:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c091ac1721..68f3c0a800 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,12 @@
- メッセージにはリアクションも可能です
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
- Misskeyネイティブでダッシュボードを実装予定です
+- Enhance: フロントエンドのエラートラッキングができるように
+ - `.config/default.yml`中の項目`sentryForFrontend`を適宜設定してください。
+ - 外部サービスであるSentryへエラー情報が送信されます。ご利用の地域の法令に従い、適切なプライバシーポリシーを策定の上で運用してください。
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
+- Enhance: アンテナでセンシティブなチャンネルのノートを除外できるように `#14177`
+- Fix: 通知のページネーションで2つ以上読み込めなくなることがある問題を修正
### Client
- Feat: 設定の管理が強化されました
@@ -56,6 +61,7 @@
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
- NOTE: 構造上クラシックUIを新しいデザインシステムに移行することが困難なため、クラシックUIが削除されました
- デッキUIでカラムを中央寄せにし、メインカラムの左右にウィジェットカラムを配置し、ナビゲーションバーを上部に表示することである程度クラシックUIを再現できます
+- Fix: iPadOSでdeck uiをマウスカーソルによってスクロールできない問題を修正
### Server
- Enhance 全体的なパフォーマンス向上
diff --git a/chart/files/default.yml b/chart/files/default.yml
index 3504e6c56c..6fb8b3eed5 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -244,6 +244,11 @@ id: "aidx"
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
+# vueIntegration:
+# tracingOptions:
+# trackComponents: true
+# browserTracingIntegration:
+# replayIntegration:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 17bfcce8e9..59a1e6dccc 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1715,6 +1715,10 @@ export interface Locale extends ILocale {
*/
"withFileAntenna": string;
/**
+ * センシティブなチャンネルのノートを非表示
+ */
+ "hideNotesInSensitiveChannel": string;
+ /**
* ブラウザへのプッシュ通知を有効にする
*/
"enableServiceworker": string;
@@ -5375,6 +5379,10 @@ export interface Locale extends ILocale {
* 上
*/
"top": string;
+ /**
+ * 埋め込み
+ */
+ "embed": string;
"_chat": {
/**
* まだメッセージはありません
@@ -5663,6 +5671,10 @@ export interface Locale extends ILocale {
* オフのとき
*/
"ifOff": string;
+ /**
+ * デバイス間でインストールしたテーマを同期
+ */
+ "enableSyncThemesBetweenDevices": string;
"_chat": {
/**
* 送信者の名前を表示
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 322506965c..947b7b4fa5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -424,6 +424,7 @@ antennaExcludeBots: "Botアカウントを除外"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ"
+hideNotesInSensitiveChannel: "センシティブなチャンネルのノートを非表示"
enableServiceworker: "ブラウザへのプッシュ通知を有効にする"
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
caseSensitive: "大文字小文字を区別する"
@@ -1339,6 +1340,7 @@ compress: "圧縮"
right: "右"
bottom: "下"
top: "上"
+embed: "埋め込み"
_chat:
noMessagesYet: "まだメッセージはありません"
@@ -1416,6 +1418,7 @@ _settings:
showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示"
ifOn: "オンのとき"
ifOff: "オフのとき"
+ enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
_chat:
showSenderName: "送信者の名前を表示"
diff --git a/package.json b/package.json
index 6b7c4a5648..c5163f8f4e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sharkey",
- "version": "2025.4.0-beta.0",
+ "version": "2025.4.0-beta.1",
"codename": "shonk",
"repository": {
"type": "git",
diff --git a/packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js b/packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js
new file mode 100644
index 0000000000..74225de96a
--- /dev/null
+++ b/packages/backend/migration/1736230492103-addAntennaHideNotesInSensitiveChannel.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class AddAntennaHideNotesInSensitiveChannel1736230492103 {
+ name = 'AddAntennaHideNotesInSensitiveChannel1736230492103'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" ADD "hideNotesInSensitiveChannel" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hideNotesInSensitiveChannel"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index ff4f2d7cf3..e67029ef7f 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -193,6 +193,7 @@
"devDependencies": {
"@jest/globals": "29.7.0",
"@nestjs/platform-express": "10.4.15",
+ "@sentry/vue": "9.8.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.37",
"@types/accepts": "1.3.7",
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 46342fd633..e2c4a75f80 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -8,7 +8,8 @@ import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
import { globSync } from 'glob';
-import * as Sentry from '@sentry/node';
+import type * as Sentry from '@sentry/node';
+import type * as SentryVue from '@sentry/vue';
import type { RedisOptions } from 'ioredis';
type RedisOptionsSource = Partial<RedisOptions> & {
@@ -66,7 +67,12 @@ type Source = {
scope?: 'local' | 'global' | string[];
};
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
- sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
+ sentryForFrontend?: {
+ options: Partial<SentryVue.BrowserOptions> & { dsn: string };
+ vueIntegration?: SentryVue.VueIntegrationOptions | null;
+ browserTracingIntegration?: Parameters<typeof SentryVue.browserTracingIntegration>[0] | null;
+ replayIntegration?: Parameters<typeof SentryVue.replayIntegration>[0] | null;
+ };
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
@@ -239,7 +245,12 @@ export type Config = {
redisForReactions: RedisOptions & RedisOptionsSource;
redisForRateLimit: RedisOptions & RedisOptionsSource;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
- sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
+ sentryForFrontend: {
+ options: Partial<SentryVue.BrowserOptions> & { dsn: string };
+ vueIntegration?: SentryVue.VueIntegrationOptions | null;
+ browserTracingIntegration?: Parameters<typeof SentryVue.browserTracingIntegration>[0] | null;
+ replayIntegration?: Parameters<typeof SentryVue.replayIntegration>[0] | null;
+ } | undefined;
perChannelMaxNoteCacheCount: number;
perUserNotificationsMaxCount: number;
deactivateAntennaThreshold: number;
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index ec9ace417e..bc7b3d2417 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -114,6 +114,8 @@ export class AntennaService implements OnApplicationShutdown {
if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false;
+ if (antenna.hideNotesInSensitiveChannel && note.channel?.isSensitive) return false;
+
if (antenna.excludeBots && noteUser.isBot) return false;
if (antenna.localOnly && noteUser.host != null) return false;
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/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 98d9571255..5543cc080d 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -595,7 +595,10 @@ export class NoteCreateService implements OnApplicationShutdown {
this.pushToTl(note, user);
- this.antennaService.addNoteToAntennas(note, user);
+ this.antennaService.addNoteToAntennas({
+ ...note,
+ channel: data.channel ?? null,
+ }, user);
if (data.reply) {
this.saveReply(data.reply, note);
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index 68ad92f396..0b699a934e 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<MiNotification, 'type', T>;
+ const createdAt = new Date();
+ let notification: FilterUnionByProperty<MiNotification, 'type', T>;
+ 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<MiNotification, 'type', T>;
+
+ 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, offset: number): string {
+ const { date, additional } = this.idService.parseFull(id);
+ return (date + offset).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<MiNotification[]> {
+ let sinceTime = sinceId ? this.toXListId(sinceId, 1) : null;
+ let untilTime = untilId ? this.toXListId(untilId, -1) : 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/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts
index e770028af3..e81c1e8db4 100644
--- a/packages/backend/src/core/entities/AntennaEntityService.ts
+++ b/packages/backend/src/core/entities/AntennaEntityService.ts
@@ -41,6 +41,7 @@ export class AntennaEntityService {
excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
+ hideNotesInSensitiveChannel: antenna.hideNotesInSensitiveChannel,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
notify: false, // 後方互換性のため
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index 9c2a5deeb8..294187feba 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -139,6 +139,7 @@ export class MetaEntityService {
policies: { ...DEFAULT_POLICIES, ...instance.policies },
+ sentryForFrontend: this.config.sentryForFrontend ?? null,
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
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/models/Antenna.ts b/packages/backend/src/models/Antenna.ts
index 33e6f48189..0d92c5cade 100644
--- a/packages/backend/src/models/Antenna.ts
+++ b/packages/backend/src/models/Antenna.ts
@@ -100,4 +100,9 @@ export class MiAntenna {
default: false,
})
public localOnly: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public hideNotesInSensitiveChannel: boolean;
}
diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts
index b5b9a5b42c..2bdaca80d0 100644
--- a/packages/backend/src/models/json-schema/antenna.ts
+++ b/packages/backend/src/models/json-schema/antenna.ts
@@ -100,5 +100,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
+ hideNotesInSensitiveChannel: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ default: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index fd735c1edd..8cc1686ac6 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -261,6 +261,38 @@ export const packedMetaLiteSchema = {
type: 'boolean',
optional: false, nullable: false,
},
+ sentryForFrontend: {
+ type: 'object',
+ optional: false, nullable: true,
+ properties: {
+ options: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ dsn: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ additionalProperties: true,
+ },
+ vueIntegration: {
+ type: 'object',
+ optional: true, nullable: true,
+ additionalProperties: true,
+ },
+ browserTracingIntegration: {
+ type: 'object',
+ optional: true, nullable: true,
+ additionalProperties: true,
+ },
+ replayIntegration: {
+ type: 'object',
+ optional: true, nullable: true,
+ additionalProperties: true,
+ },
+ },
+ },
mediaProxy: {
type: 'string',
optional: false, nullable: false,
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 9a6caa3317..a5c1bd7835 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -79,6 +79,7 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
+ hideNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@@ -139,6 +140,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
+ hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel,
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 00448297fb..57c62b7c89 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -114,6 +114,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
+ // NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
+ // https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
+
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 919a4cb3f5..a8e0ae39c5 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -78,6 +78,7 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
+ hideNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['antennaId'],
} as const;
@@ -135,6 +136,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
+ hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel,
isActive: true,
lastUsedAt: new Date(),
});
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<typeof meta, typeof paramDef> { // 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 076118d8d9..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<typeof meta, typeof paramDef> { // 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() + 1).toString() : null;
- let untilTime = ps.untilId ? (this.idService.parse(ps.untilId).date.getTime() - 1).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/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index 881e88d40f..0bd66abd64 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -146,6 +146,7 @@ describe('アンテナ', () => {
caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(),
excludeKeywords: [['']],
+ hideNotesInSensitiveChannel: false,
hasUnreadNote: false,
isActive: true,
keywords: [['keyword']],
@@ -217,6 +218,8 @@ describe('アンテナ', () => {
{ parameters: () => ({ withReplies: true }) },
{ parameters: () => ({ withFile: false }) },
{ parameters: () => ({ withFile: true }) },
+ { parameters: () => ({ hideNotesInSensitiveChannel: false }) },
+ { parameters: () => ({ hideNotesInSensitiveChannel: true }) },
];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({
@@ -626,6 +629,42 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});
+ test('が取得できること(センシティブチャンネルのノートを除く)', async () => {
+ const keyword = 'キーワード';
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]], hideNotesInSensitiveChannel: true },
+ user: alice,
+ });
+ const nonSensitiveChannel = await successfulApiCall({
+ endpoint: 'channels/create',
+ parameters: { name: 'test', isSensitive: false },
+ user: alice,
+ });
+ const sensitiveChannel = await successfulApiCall({
+ endpoint: 'channels/create',
+ parameters: { name: 'test', isSensitive: true },
+ user: alice,
+ });
+
+ const noteInLocal = await post(bob, { text: `test ${keyword}` });
+ const noteInNonSensitiveChannel = await post(bob, { text: `test ${keyword}`, channelId: nonSensitiveChannel.id });
+ await post(bob, { text: `test ${keyword}`, channelId: sensitiveChannel.id });
+
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ // 最後に投稿したものが先頭に来る。
+ const expected = [
+ noteInNonSensitiveChannel,
+ noteInLocal,
+ ];
+ assert.deepStrictEqual(response, expected);
+ });
+
+
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
test.each([
{ label: 'ID指定', offsetBy: 'id' },
diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5
index 48839241bd..5e721534d3 100644
--- a/packages/frontend-shared/themes/d-astro.json5
+++ b/packages/frontend-shared/themes/d-astro.json5
@@ -7,9 +7,9 @@
bg: '#232125',
fg: '#efdab9',
link: '#78b0a0',
- warn: '#ecb637',
+ warn: '#ffd152',
badge: '#31b1ce',
- error: '#ec4137',
+ error: '#ff6652',
focus: ':alpha<0.3<@accent',
navBg: '@panel',
navFg: '@fg',
@@ -24,7 +24,7 @@
hashtag: '#ff9156',
mention: '#ffd152',
modalBg: 'rgba(0, 0, 0, 0.5)',
- success: '#86b300',
+ success: '#78b07f',
indicator: '@accent',
mentionMe: '#fb5d38',
messageBg: '@bg',
diff --git a/packages/frontend-shared/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5
index 62208d2378..507880aab1 100644
--- a/packages/frontend-shared/themes/d-botanical.json5
+++ b/packages/frontend-shared/themes/d-botanical.json5
@@ -22,5 +22,8 @@
mentionMe: 'rgb(212, 210, 76)',
hashtag: '#5bcbb0',
link: '@accent',
+ success: '@accent',
+ warn: 'rgb(255, 213, 82)',
+ error: 'rgb(255, 105, 82)',
},
}
diff --git a/packages/frontend-shared/themes/d-persimmon.json5 b/packages/frontend-shared/themes/d-persimmon.json5
index 0ab6523dd7..538e3b7e70 100644
--- a/packages/frontend-shared/themes/d-persimmon.json5
+++ b/packages/frontend-shared/themes/d-persimmon.json5
@@ -22,5 +22,8 @@
mentionMe: '#de6161',
hashtag: '#68bad0',
link: '#a1c758',
+ error: '#ce5441',
+ warn: '#d0b868',
+ success: '#a1c758',
},
}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index a5f1604aab..907c8d68f0 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -26,6 +26,7 @@
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.1.4",
"@ruffle-rs/ruffle": "0.1.0-nightly.2024.10.15",
+ "@sentry/vue": "9.8.0",
"@syuilo/aiscript": "0.19.0",
"@transfem-org/sfm-js": "0.24.6",
"@twemoji/parser": "15.1.1",
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index defdb00568..918a3703c3 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -5,7 +5,7 @@
import { watch, version as vueVersion } from 'vue';
import { compareVersions } from 'compare-versions';
-import { version, lang, langsVersion, updateLocale, locale } from '@@/js/config.js';
+import { version, lang, langsVersion, updateLocale, locale, apiUrl } from '@@/js/config.js';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { App } from 'vue';
@@ -282,6 +282,41 @@ export async function common(createVue: () => Promise<App<Element>>) {
return root;
})();
+ if (instance.sentryForFrontend) {
+ const Sentry = await import('@sentry/vue');
+ Sentry.init({
+ app,
+ integrations: [
+ ...(instance.sentryForFrontend.vueIntegration !== undefined ? [
+ Sentry.vueIntegration(instance.sentryForFrontend.vueIntegration ?? undefined),
+ ] : []),
+ ...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? [
+ Sentry.browserTracingIntegration(instance.sentryForFrontend.browserTracingIntegration ?? undefined),
+ ] : []),
+ ...(instance.sentryForFrontend.replayIntegration !== undefined ? [
+ Sentry.replayIntegration(instance.sentryForFrontend.replayIntegration ?? undefined),
+ ] : []),
+ ],
+
+ // Set tracesSampleRate to 1.0 to capture 100%
+ tracesSampleRate: 1.0,
+
+ // Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
+ ...(instance.sentryForFrontend.browserTracingIntegration !== undefined ? {
+ tracePropagationTargets: [apiUrl],
+ } : {}),
+
+ // Capture Replay for 10% of all sessions,
+ // plus for 100% of sessions with an error
+ ...(instance.sentryForFrontend.replayIntegration !== undefined ? {
+ replaysSessionSampleRate: 0.1,
+ replaysOnErrorSampleRate: 1.0,
+ } : {}),
+
+ ...instance.sentryForFrontend.options,
+ });
+ }
+
app.mount(rootEl);
// boot.jsのやつを解除
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index ac71618ee2..e1c8200b73 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
+ <MkSwitch v-model="hideNotesInSensitiveChannel">{{ i18n.ts.hideNotesInSensitiveChannel }}</MkSwitch>
</div>
<div :class="$style.actions">
<div class="_buttons">
@@ -86,6 +87,7 @@ const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
caseSensitive: false,
localOnly: false,
withFile: false,
+ hideNotesInSensitiveChannel: false,
isActive: true,
hasUnreadNote: false,
notify: false,
@@ -108,6 +110,7 @@ const localOnly = ref<boolean>(initialAntenna.localOnly);
const excludeBots = ref<boolean>(initialAntenna.excludeBots);
const withReplies = ref<boolean>(initialAntenna.withReplies);
const withFile = ref<boolean>(initialAntenna.withFile);
+const hideNotesInSensitiveChannel = ref<boolean>(initialAntenna.hideNotesInSensitiveChannel);
const userLists = ref<Misskey.entities.UserList[] | null>(null);
watch(() => src.value, async () => {
@@ -124,6 +127,7 @@ async function saveAntenna() {
excludeBots: excludeBots.value,
withReplies: withReplies.value,
withFile: withFile.value,
+ hideNotesInSensitiveChannel: hideNotesInSensitiveChannel.value,
caseSensitive: caseSensitive.value,
localOnly: localOnly.value,
users: users.value.trim().split('\n').map(x => x.trim()),
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 3e91baada4..92f71b01af 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -249,6 +249,7 @@ async function close(skip: boolean) {
.pageFooter {
position: sticky;
+ z-index: 1;
bottom: 0;
left: 0;
flex-shrink: 0;
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 041364d4fc..22c8a6b49d 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -150,7 +150,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
},
}, {
icon: 'ti ti-code',
- text: i18n.ts.genEmbedCode,
+ text: i18n.ts.embed,
action: () => {
genEmbedCode('clips', clip.value!.id);
},
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
<div class="_gaps_m">
<SearchMarker :keywords="['sync', 'profiles', 'devices']">
<MkSwitch :modelValue="profilesSyncEnabled" @update:modelValue="changeProfilesSyncEnabled">
- <template #label><SearchLabel>{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</SearchLabel></template>
+ <template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
index f11261c05b..6de266689c 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
<div class="_gaps_m">
<SearchMarker :keywords="['sync', 'palettes', 'devices']">
<MkSwitch :modelValue="palettesSyncEnabled" @update:modelValue="changePalettesSyncEnabled">
- <template #label><SearchLabel>{{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}</SearchLabel></template>
+ <template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
</div>
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 5a96fdf0fb..88ddbb7660 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -181,6 +181,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</div>
+ <SearchMarker :keywords="['sync', 'themes', 'devices']">
+ <MkSwitch :modelValue="themesSyncEnabled" @update:modelValue="changeThemesSyncEnabled">
+ <template #label><i class="ti ti-cloud-cog"></i> <SearchLabel>{{ i18n.ts._settings.enableSyncThemesBetweenDevices }}</SearchLabel></template>
+ </MkSwitch>
+ </SearchMarker>
+
<FormSection>
<div class="_formLinksGrid">
<FormLink to="/settings/theme/manage"><template #icon><i class="ti ti-tool"></i></template>{{ i18n.ts._theme.manage }}<template #suffix>{{ themesCount }}</template></FormLink>
@@ -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/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 9b789e0d5b..77e102f239 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -56,7 +56,7 @@ const headerActions = computed(() => [{
label: i18n.ts.more,
handler: (ev: MouseEvent) => {
os.popupMenu([{
- text: i18n.ts.genEmbedCode,
+ text: i18n.ts.embed,
icon: 'ti ti-code',
action: () => {
genEmbedCode('tags', props.tag);
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 79d8b1f1d2..09d362fa42 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
<XAnnouncements v-if="$i"/>
<XStatusBars/>
-
<div :class="$style.columnsWrapper">
- <div ref="columnsEl" :class="[$style.columns, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
+ <!-- passive: https://bugs.webkit.org/show_bug.cgi?id=281300 -->
+ <div ref="columnsEl" :class="[$style.columns, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.passive.self="onWheel">
<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-for="ids in layout"
:class="$style.section"
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
- @wheel.self="onWheel"
+ @wheel.passive.self="onWheel"
>
<Suspense>
<component
@@ -175,7 +175,8 @@ window.addEventListener('resize', () => {
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'];
@@ -226,7 +227,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 09f748fcc6..0cffc628f0 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)"
>
<svg viewBox="0 0 256 128" :class="$style.tabShape">
<g transform="matrix(6.2431,0,0,6.2431,-677.417,-29.3839)">
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index bf3f0b1428..ecf8001c1f 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'],
diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts
index 723b109547..d796eb4fba 100644
--- a/packages/frontend/src/utility/get-note-menu.ts
+++ b/packages/frontend/src/utility/get-note-menu.ts
@@ -342,7 +342,7 @@ export function getNoteMenu(props: {
},
});
} else {
- menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+ menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed));
}
if (isSupportShare()) {
@@ -506,7 +506,7 @@ export function getNoteMenu(props: {
},
});
} else {
- menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode));
+ menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed));
}
}
diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
index 8eb2ef3ec9..eecd87147b 100644
--- a/packages/frontend/src/utility/get-user-menu.ts
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -197,7 +197,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
} else {
menuItems.push({
icon: 'ti ti-code',
- text: i18n.ts.genEmbedCode,
+ text: i18n.ts.embed,
type: 'parent',
children: [{
text: i18n.ts.noteOfThisUser,
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 024779499a..fd641b1e66 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",
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 1be54397b4..8677f64b71 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5165,6 +5165,8 @@ export type components = {
hasUnreadNote: boolean;
/** @default false */
notify: boolean;
+ /** @default false */
+ hideNotesInSensitiveChannel: boolean;
};
Clip: {
/**
@@ -5593,6 +5595,21 @@ export type components = {
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
+ sentryForFrontend: ({
+ options: {
+ dsn: string;
+ [key: string]: unknown;
+ };
+ vueIntegration?: {
+ [key: string]: unknown;
+ } | null;
+ browserTracingIntegration?: {
+ [key: string]: unknown;
+ } | null;
+ replayIntegration?: {
+ [key: string]: unknown;
+ } | null;
+ }) | null;
mediaProxy: string;
enableUrlPreview: boolean;
backgroundImageUrl: string | null;
@@ -12096,6 +12113,7 @@ export type operations = {
excludeBots?: boolean;
withReplies: boolean;
withFile: boolean;
+ hideNotesInSensitiveChannel?: boolean;
};
};
};
@@ -12407,6 +12425,7 @@ export type operations = {
excludeBots?: boolean;
withReplies?: boolean;
withFile?: boolean;
+ hideNotesInSensitiveChannel?: boolean;
};
};
};
@@ -23384,8 +23403,8 @@ export type operations = {
untilId?: string;
/** @default true */
markAsRead?: boolean;
- includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
- excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
+ includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
+ excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'edited' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'chatRoomInvitationReceived' | 'achievementEarned' | 'exportCompleted' | 'login' | 'createToken' | 'scheduledNoteFailed' | 'scheduledNotePosted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
};
};
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 04ee902b9f..ea350e0d68 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -544,6 +544,9 @@ importers:
'@nestjs/platform-express':
specifier: 10.4.15
version: 10.4.15(@nestjs/common@11.0.12(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.0.12)
+ '@sentry/vue':
+ specifier: 9.8.0
+ version: 9.8.0(vue@3.5.13(typescript@5.8.2))
'@simplewebauthn/types':
specifier: 12.0.0
version: 12.0.0
@@ -730,6 +733,9 @@ importers:
'@ruffle-rs/ruffle':
specifier: 0.1.0-nightly.2024.10.15
version: 0.1.0-nightly.2024.10.15
+ '@sentry/vue':
+ specifier: 9.8.0
+ version: 9.8.0(vue@3.5.13(typescript@5.8.2))
'@syuilo/aiscript':
specifier: 0.19.0
version: 0.19.0
@@ -3649,10 +3655,34 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
+ '@sentry-internal/browser-utils@9.8.0':
+ resolution: {integrity: sha512-7aQDeU9ogMLKnEBFM/vvgMMgZDkfMhoZCtX8kq65gn33L4X2B8sI5oyUj2QJtXaRSsiUjbdCaquDLqZBCaLQHA==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/feedback@9.8.0':
+ resolution: {integrity: sha512-xWiCJkD8ROuy2pnojuRLcLI6sezK399gasA5ZL4MCXdkryqZYs55Ef2Ofj4z0RdUc8gMUb81+LTqwbmbfTqNlQ==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/replay-canvas@9.8.0':
+ resolution: {integrity: sha512-/6ELOnyCOItvqv2Os29JhE8ydDds3xibMQ+FomsSkClQdC4bbc/L74nm/fdXVpJkMswtjksiTwZo1nYTS3JsIw==}
+ engines: {node: '>=18'}
+
+ '@sentry-internal/replay@9.8.0':
+ resolution: {integrity: sha512-YJhhNnrsufYVIX9s5lNSFFQrBJjUtn5AxvrcnN0fvLymNg3Y73GOUpFmhTxyELjQneKiOViClxjoWSVAN7sqQA==}
+ engines: {node: '>=18'}
+
+ '@sentry/browser@9.8.0':
+ resolution: {integrity: sha512-iFM4PGLc6qCb0GaHnA5Uy09k25RXVSepAgS574cm1CH7II1wrRjTozKnPKROW89WDMuxoTOL7Tk7qPGCyWmA4g==}
+ engines: {node: '>=18'}
+
'@sentry/core@8.55.0':
resolution: {integrity: sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==}
engines: {node: '>=14.18'}
+ '@sentry/core@9.8.0':
+ resolution: {integrity: sha512-EnN2yLWCbWjooWBPzwlXdZoJG/Bqn3ymbuXX++DUJuBGjSmtixQeTf/hKeVzj4zbib3BbbYsNBasRVjq8Rk5ng==}
+ engines: {node: '>=18'}
+
'@sentry/node@8.55.0':
resolution: {integrity: sha512-h10LJLDTRAzYgay60Oy7moMookqqSZSviCWkkmHZyaDn+4WURnPp5SKhhfrzPRQcXKrweiOwDSHBgn1tweDssg==}
engines: {node: '>=14.18'}
@@ -3673,6 +3703,16 @@ packages:
engines: {node: '>=14.18'}
hasBin: true
+ '@sentry/vue@9.8.0':
+ resolution: {integrity: sha512-E+27lL+aU8HjDo3DD3TlgStTIxBZHVqz6jZcL0/tig/JldpFRetO77terRHNfSVlPc0m3aNXuARu7G438f7ZlQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ pinia: 2.x || 3.x
+ vue: 2.x || 3.x
+ peerDependenciesMeta:
+ pinia:
+ optional: true
+
'@shikijs/core@3.2.1':
resolution: {integrity: sha512-FhsdxMWYu/C11sFisEp7FMGBtX/OSSbnXZDMBhGuUDBNTdsoZlMSgQv5f90rwvzWAdWIW6VobD+G3IrazxA6dQ==}
@@ -13747,8 +13787,36 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
+ '@sentry-internal/browser-utils@9.8.0':
+ dependencies:
+ '@sentry/core': 9.8.0
+
+ '@sentry-internal/feedback@9.8.0':
+ dependencies:
+ '@sentry/core': 9.8.0
+
+ '@sentry-internal/replay-canvas@9.8.0':
+ dependencies:
+ '@sentry-internal/replay': 9.8.0
+ '@sentry/core': 9.8.0
+
+ '@sentry-internal/replay@9.8.0':
+ dependencies:
+ '@sentry-internal/browser-utils': 9.8.0
+ '@sentry/core': 9.8.0
+
+ '@sentry/browser@9.8.0':
+ dependencies:
+ '@sentry-internal/browser-utils': 9.8.0
+ '@sentry-internal/feedback': 9.8.0
+ '@sentry-internal/replay': 9.8.0
+ '@sentry-internal/replay-canvas': 9.8.0
+ '@sentry/core': 9.8.0
+
'@sentry/core@8.55.0': {}
+ '@sentry/core@9.8.0': {}
+
'@sentry/node@8.55.0':
dependencies:
'@opentelemetry/api': 1.9.0
@@ -13808,6 +13876,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@sentry/vue@9.8.0(vue@3.5.13(typescript@5.8.2))':
+ dependencies:
+ '@sentry/browser': 9.8.0
+ '@sentry/core': 9.8.0
+ vue: 3.5.13(typescript@5.8.2)
+
'@shikijs/core@3.2.1':
dependencies:
'@shikijs/types': 3.2.1