mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-04 11:06:58 +00:00
merge from misskey-develop
This commit is contained in:
commit
dab9b518e4
54 changed files with 614 additions and 118 deletions
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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 全体的なパフォーマンス向上
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
|
@ -1714,6 +1714,10 @@ export interface Locale extends ILocale {
|
|||
* ファイルが添付されたノートのみ
|
||||
*/
|
||||
"withFileAntenna": string;
|
||||
/**
|
||||
* センシティブなチャンネルのノートを非表示
|
||||
*/
|
||||
"hideNotesInSensitiveChannel": 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": {
|
||||
/**
|
||||
* 送信者の名前を表示
|
||||
|
|
|
@ -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: "送信者の名前を表示"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "sharkey",
|
||||
"version": "2025.4.0-beta.0",
|
||||
"version": "2025.4.0-beta.1",
|
||||
"codename": "shonk",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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, // 後方互換性のため
|
||||
|
|
|
@ -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',
|
||||
|
|
40
packages/backend/src/misc/bigint.ts
Normal file
40
packages/backend/src/misc/bigint.ts
Normal file
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -100,4 +100,9 @@ export class MiAntenna {
|
|||
default: false,
|
||||
})
|
||||
public localOnly: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public hideNotesInSensitiveChannel: boolean;
|
||||
}
|
||||
|
|
|
@ -100,5 +100,10 @@ export const packedAntennaSchema = {
|
|||
optional: false, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
hideNotesInSensitiveChannel: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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 [];
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -22,5 +22,8 @@
|
|||
mentionMe: '#de6161',
|
||||
hashtag: '#68bad0',
|
||||
link: '#a1c758',
|
||||
error: '#ce5441',
|
||||
warn: '#d0b868',
|
||||
success: '#a1c758',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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のやつを解除
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -249,6 +249,7 @@ async function close(skip: boolean) {
|
|||
|
||||
.pageFooter {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
flex-shrink: 0;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(() => []);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)">
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
74
pnpm-lock.yaml
generated
74
pnpm-lock.yaml
generated
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue