diff --git a/CHANGELOG.md b/CHANGELOG.md index ee2d39cdef..c4fba1941b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,10 @@ ## 2023.10.1 ### General - Enhance: ローカルタイムライン、ソーシャルタイムラインで返信を含むかどうか設定可能に +- Feat: 絵文字申請を追加 + - これによって絵文字リクエストロールが追加されました。 + - カスタム絵文字管理の画面に 申請されている絵文字 タブが追加されました。 + - カスタム絵文字のリクエストボタンが実装されました。 ### Client - Fix: 絵文字ピッカーで横に長いカスタム絵文字が見切れる問題を修正 @@ -78,6 +82,7 @@ - Enhance: 動画再生時のデフォルトボリュームを30%に - Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正 + ### Server - Enhance: drive/files/attached-notes がページネーションに対応しました - Enhance: タイムライン取得時のパフォーマンスを大幅に向上 diff --git a/locales/index.d.ts b/locales/index.d.ts index cee9f42ca7..c249eac574 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -272,6 +272,7 @@ export interface Locale { "removed": string; "removeAreYouSure": string; "deleteAreYouSure": string; + "undraftAreYouSure": string; "resetAreYouSure": string; "saved": string; "messaging": string; @@ -676,6 +677,8 @@ export interface Locale { "regenerateLoginToken": string; "regenerateLoginTokenDescription": string; "setMultipleBySeparatingWithSpace": string; + "emojiNameValidation": string; + "isSensitive": string; "fileIdOrUrl": string; "behavior": string; "sample": string; @@ -849,8 +852,11 @@ export interface Locale { "low": string; "GamingSpeedChange": string; "GamingSpeedChangeInfo": string; + "list": string; "emailNotConfiguredWarning": string; "ratio": string; + "newEmojis": string; + "draftEmojis": string; "showVisibilityColor": string; "previewNoteText": string; "customCss": string; @@ -1004,6 +1010,7 @@ export interface Locale { "unassign": string; "color": string; "manageCustomEmojis": string; + "requestCustomEmojis": string; "youCannotCreateAnymore": string; "cannotPerformTemporary": string; "cannotPerformTemporaryDescription": string; @@ -1046,6 +1053,8 @@ export interface Locale { "sensitiveWordsDescription2": string; "notesSearchNotAvailable": string; "license": string; + "draft": string; + "undrafted": string; "unfavoriteConfirm": string; "myClips": string; "drivecleaner": string; @@ -1585,6 +1594,7 @@ export interface Locale { "inviteLimitCycle": string; "inviteExpirationTime": string; "canManageCustomEmojis": string; + "canRequestCustomEmojis": string; "driveCapacity": string; "alwaysMarkNsfw": string; "pinMax": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 102277c3a7..3506689a66 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -269,6 +269,7 @@ remove: "削除" removed: "削除しました" removeAreYouSure: "「{x}」を削除しますか?" deleteAreYouSure: "「{x}」を削除しますか?" +undraftAreYouSure: "「{x}」をドラフト解除しますか?" resetAreYouSure: "リセットしますか?" saved: "保存しました" messaging: "チャット" @@ -673,6 +674,8 @@ other: "その他" regenerateLoginToken: "ログイントークンを再生成" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" +emojiNameValidation: "名前には英数字と_が利用できます。" +isSensitive: "センシティブ" fileIdOrUrl: "ファイルIDまたはURL" behavior: "動作" sample: "サンプル" @@ -846,9 +849,12 @@ middle: "中" low: "低" GamingSpeedChange: "ゲーミングの光るスピードの調整" GamingSpeedChangeInfo: "左にすれば早くなる、右にすれば遅くなる。それだけ。" +list: "一覧" emailNotConfiguredWarning: "メールアドレスの設定がされていません。" ratio: "比率" showVisibilityColor: "ノートの公開範囲を色付けする" +newEmojis: "新しい絵文字" +draftEmojis: "申請されている絵文字" previewNoteText: "本文をプレビュー" customCss: "カスタムCSS" customCssWarn: "この設定は必ず知識のある方が行ってください。不適切な設定を行うとクライアントが正常に使用できなくなる恐れがあります。" @@ -1001,6 +1007,7 @@ assign: "アサイン" unassign: "アサインを解除" color: "色" manageCustomEmojis: "カスタム絵文字の管理" +requestCustomEmojis: "カスタム絵文字のリクエスト" youCannotCreateAnymore: "これ以上作成することはできません。" cannotPerformTemporary: "一時的に利用できません" cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。" @@ -1043,6 +1050,8 @@ sensitiveWordsDescription: "設定したワードが含まれるノートの公 sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" +draft: "ドラフト" +undrafted: "ドラフト解除" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" drivecleaner: "ドライブクリーナー" @@ -1506,6 +1515,7 @@ _role: inviteLimitCycle: "招待コードの発行間隔" inviteExpirationTime: "招待コードの有効期限" canManageCustomEmojis: "カスタム絵文字の管理" + canRequestCustomEmojis: "カスタム絵文字のリクエスト" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "ファイルにNSFWを常に付与" pinMax: "ノートのピン留めの最大数" diff --git a/packages/backend/migration/1684236161625-addEmojiDraftFlag.js b/packages/backend/migration/1684236161625-addEmojiDraftFlag.js new file mode 100644 index 0000000000..b0a13ea498 --- /dev/null +++ b/packages/backend/migration/1684236161625-addEmojiDraftFlag.js @@ -0,0 +1,11 @@ +export class AddEmojiDraftFlag1684236161625 { + name = 'AddEmojiDraftFlag1684236161625' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "draft" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "draft"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index a7786e861f..80acc4042b 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -16,10 +16,8 @@ import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; - const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @Injectable() @@ -66,6 +64,7 @@ export class CustomEmojiService implements OnApplicationShutdown { license: string | null; isSensitive: boolean; localOnly: boolean; + draft: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.insert({ @@ -82,6 +81,7 @@ export class CustomEmojiService implements OnApplicationShutdown { isSensitive: data.isSensitive, localOnly: data.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, + draft: data.draft, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); if (data.host == null) { @@ -111,6 +111,7 @@ export class CustomEmojiService implements OnApplicationShutdown { license?: string | null; isSensitive?: boolean; localOnly?: boolean; + draft: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; }, moderator?: MiUser): Promise { const emoji = await this.emojisRepository.findOneByOrFail({ id: id }); @@ -125,6 +126,7 @@ export class CustomEmojiService implements OnApplicationShutdown { license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, + draft: data.draft, originalUrl: data.driveFile != null ? data.driveFile.url : undefined, publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined, type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined, diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index c8734cb0b2..be4f874803 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -33,6 +33,7 @@ export type RolePolicies = { inviteLimitCycle: number; inviteExpirationTime: number; canManageCustomEmojis: boolean; + canRequestCustomEmojis: boolean; canSearchNotes: boolean; canUseTranslator: boolean; canHideAds: boolean; @@ -59,6 +60,7 @@ export const DEFAULT_POLICIES: RolePolicies = { inviteLimitCycle: 60 * 24 * 7, inviteExpirationTime: 0, canManageCustomEmojis: false, + canRequestCustomEmojis: false, canSearchNotes: false, canUseTranslator: true, canHideAds: false, @@ -303,6 +305,7 @@ export class RoleService implements OnApplicationShutdown { inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), + canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 4d7e14f683..e1230012be 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -19,7 +19,13 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { WebhookService } from '@/core/WebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { + FollowingsRepository, + FollowRequestsRepository, + InstancesRepository, + UserProfilesRepository, + UsersRepository, +} from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; @@ -53,25 +59,18 @@ export class UserFollowingService implements OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.config) private config: Config, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, - @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private cacheService: CacheService, private utilityService: UtilityService, private userEntityService: UserEntityService, @@ -197,10 +196,18 @@ export class UserFollowingService implements OnModuleInit { @bindThis private async insertFollowingDoc( followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox'] }, follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'] + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox'] }, silent = false, withReplies?: boolean, @@ -247,8 +254,7 @@ export class UserFollowingService implements OnModuleInit { }); // 通知を作成 - this.notificationService.createNotification(follower.id, 'followRequestAccepted', { - }, followee.id); + this.notificationService.createNotification(follower.id, 'followRequestAccepted', {}, followee.id); } if (alreadyFollowed) return; @@ -322,18 +328,25 @@ export class UserFollowingService implements OnModuleInit { }); // 通知を作成 - this.notificationService.createNotification(followee.id, 'follow', { - }, follower.id); + this.notificationService.createNotification(followee.id, 'follow', {}, follower.id); } } @bindThis public async unfollow( follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox']; }, followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox']; }, silent = false, ): Promise { @@ -464,10 +477,18 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async createFollowRequest( follower: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox']; }, followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox']; }, requestId?: string, withReplies?: boolean, @@ -560,7 +581,11 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async acceptFollowRequest( followee: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox']; }, follower: MiUser, ): Promise { @@ -588,7 +613,11 @@ export class UserFollowingService implements OnModuleInit { @bindThis public async acceptAllFollowRequests( user: { - id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']; + id: MiUser['id']; + host: MiUser['host']; + uri: MiUser['host']; + inbox: MiUser['inbox']; + sharedInbox: MiUser['sharedInbox']; }, ): Promise { const requests = await this.followRequestsRepository.findBy({ diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index 5b97cfad5e..d50d7356ae 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -33,6 +33,7 @@ export class EmojiEntityService { url: emoji.publicUrl || emoji.originalUrl, isSensitive: emoji.isSensitive ? true : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, + draft: emoji.draft, }; } @@ -61,6 +62,7 @@ export class EmojiEntityService { isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + draft: emoji.draft, }; } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index 563ac1d9d3..bd8d54ffc3 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -81,4 +81,10 @@ export class MiEmoji { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisEmojiAsReaction: string[]; + + @Column('boolean', { + default: false, + nullable: false, + }) + public draft: boolean; } diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 99a58f8773..90054cbc50 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -40,6 +40,10 @@ export const packedEmojiSimpleSchema = { format: 'id', }, }, + draft: { + type: 'boolean', + optional: false, nullable: true, + }, }, } as const; @@ -81,6 +85,10 @@ export const packedEmojiDetailedSchema = { type: 'string', optional: false, nullable: true, }, + draft: { + type: 'boolean', + optional: false, nullable: true, + }, isSensitive: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index a52af54a39..088596bba6 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -103,6 +103,7 @@ export class ImportCustomEmojisProcessorService { isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: [], + draft: false, }); } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index b4cdc196f8..79164e51df 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -27,6 +27,7 @@ import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-al import * as ep___admin_emoji_setlocalOnlyBulk from './endpoints/admin/emoji/set-localonly-bulk.js'; import * as ep___admin_emoji_setisSensitiveBulk from './endpoints/admin/emoji/set-issensitive-bulk.js'; import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_addDraft from './endpoints/admin/emoji/add-draft.js'; import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; @@ -382,6 +383,7 @@ const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-ali const $admin_emoji_setlocalOnlyBulk: Provider = { provide: 'ep:admin/emoji/set-localonly-bulk', useClass: ep___admin_emoji_setlocalOnlyBulk.default }; const $admin_emoji_setisSensitiveBulk: Provider = { provide: 'ep:admin/emoji/set-issensitive-bulk', useClass: ep___admin_emoji_setisSensitiveBulk.default }; const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.default }; +const $admin_emoji_addDraft: Provider = { provide: 'ep:admin/emoji/add-draft', useClass: ep___admin_emoji_addDraft.default }; const $admin_emoji_copy: Provider = { provide: 'ep:admin/emoji/copy', useClass: ep___admin_emoji_copy.default }; const $admin_emoji_deleteBulk: Provider = { provide: 'ep:admin/emoji/delete-bulk', useClass: ep___admin_emoji_deleteBulk.default }; const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default }; @@ -741,6 +743,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_emoji_setlocalOnlyBulk, $admin_emoji_setisSensitiveBulk, $admin_emoji_add, + $admin_emoji_addDraft, $admin_emoji_copy, $admin_emoji_deleteBulk, $admin_emoji_delete, @@ -1092,6 +1095,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_drive_showFile, $admin_emoji_addAliasesBulk, $admin_emoji_add, + $admin_emoji_addDraft, $admin_emoji_copy, $admin_emoji_deleteBulk, $admin_emoji_delete, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 3fac9552ad..c2ab5d85fa 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -26,6 +26,7 @@ import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js'; import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js'; import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js'; +import * as ep___admin_emoji_addDraft from './endpoints/admin/emoji/add-draft.js'; import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js'; import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js'; import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js'; @@ -377,6 +378,7 @@ const eps = [ ['admin/drive/show-file', ep___admin_drive_showFile], ['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk], ['admin/emoji/add', ep___admin_emoji_add], + ['admin/emoji/add-draft', ep___admin_emoji_addDraft], ['admin/emoji/copy', ep___admin_emoji_copy], ['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk], ['admin/emoji/delete', ep___admin_emoji_delete], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-draft.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-draft.ts new file mode 100644 index 0000000000..7088f801e9 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-draft.ts @@ -0,0 +1,80 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { DriveFilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { ApiError } from '../../../error.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireRolePolicy: 'canRequestCustomEmojis', + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' }, + category: { + type: 'string', + nullable: true, + description: 'Use `null` to reset the category.', + }, + aliases: { type: 'array', items: { + type: 'string', + } }, + license: { type: 'string', nullable: true }, + fileId: { type: 'string', format: 'misskey:id' }, + isSensitive: { type: 'boolean' }, + localOnly: { type: 'boolean' }, + }, + required: ['name', 'fileId'], +} as const; + +// TODO: ロジックをサービスに切り出す + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private customEmojiService: CustomEmojiService, + + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); + + const emoji = await this.customEmojiService.add({ + driveFile, + name: ps.name, + category: ps.category ?? null, + aliases: ps.aliases ?? [], + license: ps.license ?? null, + host: null, + draft: true, + isSensitive: ps.isSensitive ?? false, + localOnly: ps.localOnly ?? false, + roleIdsThatCanBeUsedThisEmojiAsReaction: [], + }); + + return { + id: emoji.id, + }; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index faab8ee608..7f4474419c 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -41,17 +41,21 @@ export const paramDef = { nullable: true, description: 'Use `null` to reset the category.', }, - aliases: { type: 'array', items: { - type: 'string', - } }, + aliases: { + type: 'array', items: { + type: 'string', + }, + }, license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, - roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { - type: 'string', - } }, + roleIdsThatCanBeUsedThisEmojiAsReaction: { + type: 'array', items: { + type: 'string', + }, + }, }, - required: ['name', 'fileId'], + required: ['name', 'fileId', 'draft'], } as const; // TODO: ロジックをサービスに切り出す @@ -61,13 +65,12 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private customEmojiService: CustomEmojiService, - private emojiEntityService: EmojiEntityService, ) { super(meta, paramDef, async (ps, me) => { const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (driveFile == null) throw new ApiError(meta.errors.noSuchFile); const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); if (isDuplicate) throw new ApiError(meta.errors.duplicateName); @@ -81,6 +84,7 @@ export default class extends Endpoint { // eslint- license: ps.license ?? null, isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, + draft: false, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], }, me); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index ab16d86a3d..8fba829c5e 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -64,6 +64,7 @@ export const paramDef = { type: 'object', properties: { query: { type: 'string', nullable: true, default: null }, + draft: { type: 'boolean', nullable: true, default: null }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -86,6 +87,14 @@ export default class extends Endpoint { // eslint- let emojis: MiEmoji[]; + if (ps.draft !== null) { + if (ps.draft) { + q.andWhere('emoji.draft = TRUE'); + } else { + q.andWhere('emoji.draft = FALSE'); + } + } + if (ps.query) { //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); //const emojis = await q.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 0bcf30a1c6..7512e88c05 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -60,8 +60,9 @@ export const paramDef = { roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', } }, + draft: { type: 'boolean' }, }, - required: ['id', 'name', 'aliases'], + required: ['id', 'name', 'draft', 'aliases'], } as const; @Injectable() @@ -97,6 +98,7 @@ export default class extends Endpoint { // eslint- isSensitive: ps.isSensitive, localOnly: ps.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, + draft: ps.draft, }, me); }); } diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 7c4f910559..f8b655e772 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -96,6 +96,10 @@ const emojiDb = computed(() => { const customEmojiDB: EmojiDef[] = []; for (const x of customEmojis.value) { + if (x.draft) { + continue; + } + customEmojiDB.push({ name: x.name, emoji: `:${x.name}:`, diff --git a/packages/frontend/src/components/MkCustomEmojiEditDraft.vue b/packages/frontend/src/components/MkCustomEmojiEditDraft.vue new file mode 100644 index 0000000000..34d033119d --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiEditDraft.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/packages/frontend/src/components/MkCustomEmojiEditLocal.vue b/packages/frontend/src/components/MkCustomEmojiEditLocal.vue new file mode 100644 index 0000000000..7112a38430 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiEditLocal.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/packages/frontend/src/components/MkCustomEmojiEditRemote.vue b/packages/frontend/src/components/MkCustomEmojiEditRemote.vue new file mode 100644 index 0000000000..26c8dd66ac --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiEditRemote.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/components/MkEmojiEditDialog.vue similarity index 72% rename from packages/frontend/src/pages/emoji-edit-dialog.vue rename to packages/frontend/src/components/MkEmojiEditDialog.vue index 2e6050490e..43b03971e7 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/components/MkEmojiEditDialog.vue @@ -7,10 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -33,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.selectFile }} + @@ -44,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -61,13 +64,19 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}
- isSensitive + {{ i18n.ts.isSensitive }} {{ i18n.ts.localOnly }} - {{ i18n.ts.delete }} + + {{ i18n.ts.draft }} +
- {{ props.emoji ? i18n.ts.update : i18n.ts.create }} +
+ {{ i18n.ts.delete }} + {{ props.emoji ? i18n.ts.update : i18n.ts.create }} + {{ props.emoji ? i18n.ts.update : i18n.ts.create }} +
@@ -76,6 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue index 9aaa7890a9..6cd7b1bbb0 100644 --- a/packages/frontend/src/pages/emojis.emoji.vue +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -4,10 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only -->