Merge branch 'develop' into re-ed25519

This commit is contained in:
tamaina 2024-09-26 14:15:56 +09:00
commit 310b3a563d
26 changed files with 220 additions and 16 deletions

View File

@ -7,7 +7,10 @@
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) - Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように - Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
- Feat: データエクスポートが完了した際に通知を発行するように
- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように - Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
- Enhance: 依存関係の更新
- Enhance: l10nの更新
### Client ### Client
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように - Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
@ -15,6 +18,7 @@
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく - Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
- Enhance: ScratchpadにUIインスペクターを追加 - Enhance: ScratchpadにUIインスペクターを追加
- Enhance: Play編集画面の項目の並びを少しリデザイン - Enhance: Play編集画面の項目の並びを少しリデザイン
- Enhance: 各種メニューをドロワー表示するかどうか設定可能に
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 - Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正 - Fix: 月の違う同じ日はセパレータが表示されないのを修正

20
locales/index.d.ts vendored
View File

@ -1352,6 +1352,10 @@ export interface Locale extends ILocale {
* *
*/ */
"addFile": string; "addFile": string;
/**
*
*/
"showFile": string;
/** /**
* *
*/ */
@ -2056,6 +2060,10 @@ export interface Locale extends ILocale {
* *
*/ */
"menuStyle": string; "menuStyle": string;
/**
*
*/
"style": string;
/** /**
* *
*/ */
@ -9249,6 +9257,10 @@ export interface Locale extends ILocale {
* *
*/ */
"flushNotification": string; "flushNotification": string;
/**
* {x}
*/
"exportOfXCompleted": ParameterizedString<"x">;
"_types": { "_types": {
/** /**
* *
@ -9302,6 +9314,14 @@ export interface Locale extends ILocale {
* *
*/ */
"achievementEarned": string; "achievementEarned": string;
/**
*
*/
"exportCompleted": string;
/**
*
*/
"test": string;
/** /**
* *
*/ */

View File

@ -334,6 +334,7 @@ renameFolder: "フォルダー名を変更"
deleteFolder: "フォルダーを削除" deleteFolder: "フォルダーを削除"
folder: "フォルダー" folder: "フォルダー"
addFile: "ファイルを追加" addFile: "ファイルを追加"
showFile: "ファイルを表示"
emptyDrive: "ドライブは空です" emptyDrive: "ドライブは空です"
emptyFolder: "フォルダーは空です" emptyFolder: "フォルダーは空です"
unableToDelete: "削除できません" unableToDelete: "削除できません"
@ -510,6 +511,7 @@ aboutX: "{x}について"
emojiStyle: "絵文字のスタイル" emojiStyle: "絵文字のスタイル"
native: "ネイティブ" native: "ネイティブ"
menuStyle: "メニューのスタイル" menuStyle: "メニューのスタイル"
style: "スタイル"
drawer: "ドロワー" drawer: "ドロワー"
popup: "ポップアップ" popup: "ポップアップ"
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する" showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
@ -2442,6 +2444,7 @@ _notification:
renotedBySomeUsers: "{n}人がリノートしました" renotedBySomeUsers: "{n}人がリノートしました"
followedBySomeUsers: "{n}人にフォローされました" followedBySomeUsers: "{n}人にフォローされました"
flushNotification: "通知の履歴をリセットする" flushNotification: "通知の履歴をリセットする"
exportOfXCompleted: "{x}のエクスポートが完了しました"
_types: _types:
all: "すべて" all: "すべて"
@ -2457,6 +2460,8 @@ _notification:
followRequestAccepted: "フォローが受理された" followRequestAccepted: "フォローが受理された"
roleAssigned: "ロールが付与された" roleAssigned: "ロールが付与された"
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
test: "通知のテスト"
app: "連携アプリからの通知" app: "連携アプリからの通知"
_actions: _actions:

View File

@ -162,6 +162,10 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'achievementEarned' ? { ...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement, achievement: notification.achievement,
} : {}), } : {}),
...(notification.type === 'exportCompleted' ? {
exportedEntity: notification.exportedEntity,
fileId: notification.fileId,
} : {}),
...(notification.type === 'app' ? { ...(notification.type === 'app' ? {
body: notification.customBody, body: notification.customBody,
header: notification.customHeader, header: notification.customHeader,

View File

@ -7,6 +7,8 @@ import { MiUser } from './User.js';
import { MiNote } from './Note.js'; import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js'; import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js'; import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js';
import { userExportableEntities } from '@/types.js';
export type MiNotification = { export type MiNotification = {
type: 'note'; type: 'note';
@ -77,6 +79,12 @@ export type MiNotification = {
id: string; id: string;
createdAt: string; createdAt: string;
achievement: string; achievement: string;
} | {
type: 'exportCompleted';
id: string;
createdAt: string;
exportedEntity: typeof userExportableEntities[number];
fileId: MiDriveFile['id'];
} | { } | {
type: 'app'; type: 'app';
id: string; id: string;

View File

@ -4,7 +4,7 @@
*/ */
import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js'; import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import { notificationTypes } from '@/types.js'; import { notificationTypes, userExportableEntities } from '@/types.js';
const baseSchema = { const baseSchema = {
type: 'object', type: 'object',
@ -298,6 +298,26 @@ export const packedNotificationSchema = {
enum: ACHIEVEMENT_TYPES, enum: ACHIEVEMENT_TYPES,
}, },
}, },
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['exportCompleted'],
},
exportedEntity: {
type: 'string',
optional: false, nullable: false,
enum: userExportableEntities,
},
fileId: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
},
}, { }, {
type: 'object', type: 'object',
properties: { properties: {

View File

@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DBExportAntennasData } from '../types.js'; import type { DBExportAntennasData } from '../types.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -35,6 +36,7 @@ export class ExportAntennasProcessorService {
private driveService: DriveService, private driveService: DriveService,
private utilityService: UtilityService, private utilityService: UtilityService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas'); this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
} }
@ -95,6 +97,11 @@ export class ExportAntennasProcessorService {
const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ('Exported to: ' + driveFile.id); this.logger.succ('Exported to: ' + driveFile.id);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'antenna',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -30,6 +31,7 @@ export class ExportBlockingProcessorService {
private blockingsRepository: BlockingsRepository, private blockingsRepository: BlockingsRepository,
private utilityService: UtilityService, private utilityService: UtilityService,
private notificationService: NotificationService,
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
@ -109,6 +111,11 @@ export class ExportBlockingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'blocking',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js'; import type { DbJobDataWithUser } from '../types.js';
@ -43,6 +44,7 @@ export class ExportClipsProcessorService {
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private idService: IdService, private idService: IdService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips'); this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
} }
@ -79,6 +81,11 @@ export class ExportClipsProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'clip',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -16,6 +16,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js'; import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js'; import { DownloadService } from '@/core/DownloadService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService {
private driveService: DriveService, private driveService: DriveService,
private downloadService: DownloadService, private downloadService: DownloadService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis'); this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
} }
@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService {
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true }); const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'customEmoji',
fileId: driveFile.id,
});
cleanup(); cleanup();
archiveCleanup(); archiveCleanup();
resolve(); resolve();

View File

@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js'; import type { DbJobDataWithUser } from '../types.js';
@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private idService: IdService, private idService: IdService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites'); this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
} }
@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'favorite',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import type { MiFollowing } from '@/models/Following.js'; import type { MiFollowing } from '@/models/Following.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -36,6 +37,7 @@ export class ExportFollowingProcessorService {
private utilityService: UtilityService, private utilityService: UtilityService,
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-following'); this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
} }
@ -113,6 +115,11 @@ export class ExportFollowingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'following',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -32,6 +33,7 @@ export class ExportMutingProcessorService {
private utilityService: UtilityService, private utilityService: UtilityService,
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-muting'); this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
} }
@ -110,6 +112,11 @@ export class ExportMutingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'muting',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { JsonArrayStream } from '@/misc/JsonArrayStream.js'; import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
import { FileWriterStream } from '@/misc/FileWriterStream.js'; import { FileWriterStream } from '@/misc/FileWriterStream.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
@ -112,6 +113,7 @@ export class ExportNotesProcessorService {
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private idService: IdService, private idService: IdService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes'); this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
} }
@ -150,6 +152,11 @@ export class ExportNotesProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'note',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js'; import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js'; import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
@ -35,6 +36,7 @@ export class ExportUserListsProcessorService {
private utilityService: UtilityService, private utilityService: UtilityService,
private driveService: DriveService, private driveService: DriveService,
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
private notificationService: NotificationService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists'); this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists');
} }
@ -89,6 +91,11 @@ export class ExportUserListsProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' }); const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`); this.logger.succ(`Exported to: ${driveFile.id}`);
this.notificationService.createNotification(user.id, 'exportCompleted', {
exportedEntity: 'userList',
fileId: driveFile.id,
});
} finally { } finally {
cleanup(); cleanup();
} }

View File

@ -16,6 +16,7 @@
* followRequestAccepted - * followRequestAccepted -
* roleAssigned - * roleAssigned -
* achievementEarned - * achievementEarned -
* exportCompleted -
* app - * app -
* test - * test -
*/ */
@ -32,6 +33,7 @@ export const notificationTypes = [
'followRequestAccepted', 'followRequestAccepted',
'roleAssigned', 'roleAssigned',
'achievementEarned', 'achievementEarned',
'exportCompleted',
'app', 'app',
'test', 'test',
] as const; ] as const;
@ -51,6 +53,20 @@ export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
export const followingVisibilities = ['public', 'followers', 'private'] as const; export const followingVisibilities = ['public', 'followers', 'private'] as const;
export const followersVisibilities = ['public', 'followers', 'private'] as const; export const followersVisibilities = ['public', 'followers', 'private'] as const;
/**
*
*
* 使DBの名称等と必ずしも一致しない
*/
export const userExportableEntities = ['antenna', 'blocking', 'clip', 'customEmoji', 'favorite', 'following', 'muting', 'note', 'userList'] as const;
/**
*
*
* 使DBの名称等と必ずしも一致しない
*/
export const userImportableEntities = ['antenna', 'blocking', 'customEmoji', 'following', 'muting', 'userList'] as const;
export const moderationLogTypes = [ export const moderationLogTypes = [
'updateServerSettings', 'updateServerSettings',
'suspend', 'suspend',

View File

@ -67,6 +67,8 @@ export const notificationTypes = [
'followRequestAccepted', 'followRequestAccepted',
'roleAssigned', 'roleAssigned',
'achievementEarned', 'achievementEarned',
'exportCompleted',
'test',
'app', 'app',
] as const; ] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;

View File

@ -229,6 +229,7 @@ function onMousedown(evt: MouseEvent): void {
} }
&.danger { &.danger {
font-weight: bold;
color: #ff2a2a; color: #ff2a2a;
&.primary { &.primary {

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="modal" ref="modal"
v-slot="{ type, maxHeight }" v-slot="{ type, maxHeight }"
:zPriority="'middle'" :zPriority="'middle'"
:preferType="defaultStore.state.emojiPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :preferType="defaultStore.state.emojiPickerStyle"
:hasInteractionWithOtherFocusTrappedEls="true" :hasInteractionWithOtherFocusTrappedEls="true"
:transparentBg="true" :transparentBg="true"
:manualShowing="manualShowing" :manualShowing="manualShowing"

View File

@ -13,7 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/> <MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="'icon' in notification" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/> <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div <div
:class="[$style.subIcon, { :class="[$style.subIcon, {
[$style.t_follow]: notification.type === 'follow', [$style.t_follow]: notification.type === 'follow',
@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_quote]: notification.type === 'quote', [$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null, [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
}]" }]"
> >
@ -37,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<template v-else-if="notification.type === 'roleAssigned'"> <template v-else-if="notification.type === 'roleAssigned'">
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i> <i v-else class="ti ti-badges"></i>
@ -57,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
@ -98,6 +102,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }} {{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA> </MkA>
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
{{ i18n.ts.showFile }}
</MkA>
<template v-else-if="notification.type === 'follow'"> <template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
</template> </template>
@ -161,6 +168,20 @@ const props = withDefaults(defineProps<{
full: false, full: false,
}); });
type ExportCompletedNotification = Misskey.entities.Notification & { type: 'exportCompleted' };
const exportEntityName = {
antenna: i18n.ts.antennas,
blocking: i18n.ts.blockedUsers,
clip: i18n.ts.clips,
customEmoji: i18n.ts.customEmojis,
favorite: i18n.ts.favorites,
following: i18n.ts.following,
muting: i18n.ts.mutedUsers,
note: i18n.ts.notes,
userList: i18n.ts.lists,
} as const satisfies Record<ExportCompletedNotification['exportedEntity'], string>;
const followRequestDone = ref(false); const followRequestDone = ref(false);
const acceptFollowRequest = () => { const acceptFollowRequest = () => {
@ -298,6 +319,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none; pointer-events: none;
} }
.t_exportCompleted {
padding: 3px;
background: var(--eventOther);
pointer-events: none;
}
.t_roleAssigned { .t_roleAssigned {
padding: 3px; padding: 3px;
background: var(--eventOther); background: var(--eventOther);

View File

@ -113,10 +113,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<option :value="4">{{ i18n.ts.large }}+</option> <option :value="4">{{ i18n.ts.large }}+</option>
</MkRadios> </MkRadios>
<MkSwitch v-model="emojiPickerUseDrawerForMobile"> <MkSelect v-model="emojiPickerStyle">
{{ i18n.ts.useDrawerReactionPickerForMobile }} <template #label>{{ i18n.ts.style }}</template>
<template #caption>{{ i18n.ts.needReloadToApply }}</template> <template #caption>{{ i18n.ts.needReloadToApply }}</template>
</MkSwitch> <option value="auto">{{ i18n.ts.auto }}</option>
<option value="popup">{{ i18n.ts.popup }}</option>
<option value="drawer">{{ i18n.ts.drawer }}</option>
</MkSelect>
</div> </div>
</FormSection> </FormSection>
</div> </div>
@ -128,7 +131,7 @@ import Sortable from 'vuedraggable';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSelect from '@/components/MkSelect.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -146,7 +149,7 @@ const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmoji
const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale')); const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth')); const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight')); const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
const emojiPickerUseDrawerForMobile = computed(defaultStore.makeGetterSetter('emojiPickerUseDrawerForMobile')); const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle'));
const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev); const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev); const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);

View File

@ -73,7 +73,7 @@ import { notificationTypes } from '@@/js/const.js';
const $i = signinRequired(); const $i = signinRequired();
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned']; const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned', 'test', 'exportCompleted'] as const satisfies (typeof notificationTypes[number])[];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer); const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);

View File

@ -87,7 +87,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'emojiPickerScale', 'emojiPickerScale',
'emojiPickerWidth', 'emojiPickerWidth',
'emojiPickerHeight', 'emojiPickerHeight',
'emojiPickerUseDrawerForMobile', 'emojiPickerStyle',
'defaultSideView', 'defaultSideView',
'menuDisplay', 'menuDisplay',
'reportError', 'reportError',

View File

@ -304,9 +304,9 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: 2, default: 2,
}, },
emojiPickerUseDrawerForMobile: { emojiPickerStyle: {
where: 'device', where: 'device',
default: true, default: 'auto' as 'auto' | 'popup' | 'drawer',
}, },
recentlyUsedEmojis: { recentlyUsedEmojis: {
where: 'device', where: 'device',

View File

@ -4274,6 +4274,17 @@ export type components = {
type: 'achievementEarned'; type: 'achievementEarned';
/** @enum {string} */ /** @enum {string} */
achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead'; achievement: 'notes1' | 'notes10' | 'notes100' | 'notes500' | 'notes1000' | 'notes5000' | 'notes10000' | 'notes20000' | 'notes30000' | 'notes40000' | 'notes50000' | 'notes60000' | 'notes70000' | 'notes80000' | 'notes90000' | 'notes100000' | 'login3' | 'login7' | 'login15' | 'login30' | 'login60' | 'login100' | 'login200' | 'login300' | 'login400' | 'login500' | 'login600' | 'login700' | 'login800' | 'login900' | 'login1000' | 'passedSinceAccountCreated1' | 'passedSinceAccountCreated2' | 'passedSinceAccountCreated3' | 'loggedInOnBirthday' | 'loggedInOnNewYearsDay' | 'noteClipped1' | 'noteFavorited1' | 'myNoteFavorited1' | 'profileFilled' | 'markedAsCat' | 'following1' | 'following10' | 'following50' | 'following100' | 'following300' | 'followers1' | 'followers10' | 'followers50' | 'followers100' | 'followers300' | 'followers500' | 'followers1000' | 'collectAchievements30' | 'viewAchievements3min' | 'iLoveMisskey' | 'foundTreasure' | 'client30min' | 'client60min' | 'noteDeletedWithin1min' | 'postedAtLateNight' | 'postedAt0min0sec' | 'selfQuote' | 'htl20npm' | 'viewInstanceChart' | 'outputHelloWorldOnScratchpad' | 'open3windows' | 'driveFolderCircularReference' | 'reactWithoutRead' | 'clickedClickHere' | 'justPlainLucky' | 'setNameToSyuilo' | 'cookieClicked' | 'brainDiver' | 'smashTestNotificationButton' | 'tutorialCompleted' | 'bubbleGameExplodingHead' | 'bubbleGameDoubleExplodingHead';
}) | ({
/** Format: id */
id: string;
/** Format: date-time */
createdAt: string;
/** @enum {string} */
type: 'exportCompleted';
/** @enum {string} */
exportedEntity: 'antenna' | 'blocking' | 'clip' | 'customEmoji' | 'favorite' | 'following' | 'muting' | 'note' | 'userList';
/** Format: id */
fileId: string;
}) | ({ }) | ({
/** Format: id */ /** Format: id */
id: string; id: string;
@ -18543,8 +18554,8 @@ export type operations = {
untilId?: string; untilId?: string;
/** @default true */ /** @default true */
markAsRead?: boolean; markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'pollVote' | 'groupInvited')[]; excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'pollVote' | 'groupInvited')[];
}; };
}; };
}; };
@ -18611,8 +18622,8 @@ export type operations = {
untilId?: string; untilId?: string;
/** @default true */ /** @default true */
markAsRead?: boolean; markAsRead?: boolean;
includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; includeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[]; excludeTypes?: ('note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'exportCompleted' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped' | 'pollVote' | 'groupInvited')[];
}; };
}; };
}; };

View File

@ -210,6 +210,25 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
tag: `achievement:${data.body.achievement}`, tag: `achievement:${data.body.achievement}`,
}]; }];
case 'exportCompleted': {
const entityName = {
antenna: i18n.ts.antennas,
blocking: i18n.ts.blockedUsers,
clip: i18n.ts.clips,
customEmoji: i18n.ts.customEmojis,
favorite: i18n.ts.favorites,
following: i18n.ts.following,
muting: i18n.ts.mutedUsers,
note: i18n.ts.notes,
userList: i18n.ts.lists,
} as const satisfies Record<typeof data.body.exportedEntity, string>;
return [i18n.tsx._notification.exportOfXCompleted({ x: entityName[data.body.exportedEntity] }), {
badge: iconUrl('circle-check'),
data,
}];
}
case 'pollEnded': case 'pollEnded':
return [i18n.ts._notification.pollEnded, { return [i18n.ts._notification.pollEnded, {
body: data.body.note.text ?? '', body: data.body.note.text ?? '',