diff --git a/locales/en-US.yml b/locales/en-US.yml index 6bc970b705..8327045697 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1052,6 +1052,7 @@ hiddenTags: "Hidden hashtags" hiddenTagsDescription: "Select tags which will not shown on trend list.\nMultiple tags could be registered by lines." notesSearchNotAvailable: "Note search is unavailable." license: "License" +request: "Request" unfavoriteConfirm: "Really remove from favorites?" myClips: "My clips" drivecleaner: "Drive Cleaner" diff --git a/locales/index.d.ts b/locales/index.d.ts index 4117b81568..a50828b682 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4220,6 +4220,10 @@ export interface Locale extends ILocale { * ライセンス */ "license": string; + /** + * リクエスト + */ + "request": string; /** * お気に入り解除しますか? */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 48cc48c218..22d8c21da1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1051,6 +1051,7 @@ hiddenTags: "非表示ハッシュタグ" hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。" notesSearchNotAvailable: "ノート検索は利用できません。" license: "ライセンス" +request: "リクエスト" unfavoriteConfirm: "お気に入り解除しますか?" myClips: "自分のクリップ" drivecleaner: "ドライブクリーナー" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 0de32907f7..ee3c4cdc10 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1051,6 +1051,7 @@ hiddenTags: "숨긴 해시태그" hiddenTagsDescription: "설정한 태그를 트렌드에 표시하지 않도록 합니다. 줄 바꿈으로 하나씩 나눠서 설정할 수 있습니다." notesSearchNotAvailable: "노트 검색을 이용하실 수 없습니다." license: "라이선스" +request: "요청" unfavoriteConfirm: "즐겨찾기를 해제하시겠습니까?" myClips: "내 클립" drivecleaner: "드라이브 정리" diff --git a/packages/backend/migration/1706723072096-emoji-more-fields.js b/packages/backend/migration/1706723072096-emoji-more-fields.js new file mode 100644 index 0000000000..49cc58c325 --- /dev/null +++ b/packages/backend/migration/1706723072096-emoji-more-fields.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class EmojiMoreFields1706723072096 { + name = 'EmojiMoreFields1706723072096' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" ADD "requestedBy" character varying(1024)`); + await queryRunner.query(`ALTER TABLE "emoji" ADD "memo" character varying(8192) NOT NULL DEFAULT ''`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "memo"`); + await queryRunner.query(`ALTER TABLE "emoji" DROP COLUMN "requestedBy"`); + } +} diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 69edbed79f..6724748130 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -66,6 +66,8 @@ export class CustomEmojiService implements OnApplicationShutdown { license: string | null; isSensitive: boolean; localOnly: boolean; + requestedBy: string | null; + memo: string | null; roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][]; roleIdsThatCanNotBeUsedThisEmojiAsReaction: MiRole['id'][]; }, moderator?: MiUser): Promise { @@ -82,6 +84,8 @@ export class CustomEmojiService implements OnApplicationShutdown { license: data.license, isSensitive: data.isSensitive, localOnly: data.localOnly, + requestedBy: data.requestedBy, + memo: data.memo, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction, roleIdsThatCanNotBeUsedThisEmojiAsReaction: data.roleIdsThatCanNotBeUsedThisEmojiAsReaction, }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); @@ -113,6 +117,8 @@ export class CustomEmojiService implements OnApplicationShutdown { license?: string | null; isSensitive?: boolean; localOnly?: boolean; + requestedBy?: string | null; + memo?: string | null; roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][]; roleIdsThatCanNotBeUsedThisEmojiAsReaction?: MiRole['id'][]; }, moderator?: MiUser): Promise { @@ -131,6 +137,8 @@ export class CustomEmojiService implements OnApplicationShutdown { 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, + requestedBy: data.requestedBy ?? undefined, + memo: data.memo ?? undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined, roleIdsThatCanNotBeUsedThisEmojiAsReaction: data.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? undefined, }); diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts index ea03a11afd..110ab395b7 100644 --- a/packages/backend/src/core/entities/EmojiEntityService.ts +++ b/packages/backend/src/core/entities/EmojiEntityService.ts @@ -9,12 +9,15 @@ import type { EmojisRepository } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import type { MiEmoji } from '@/models/Emoji.js'; import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; @Injectable() export class EmojiEntityService { constructor( @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + + private idService: IdService, ) { } @@ -75,4 +78,39 @@ export class EmojiEntityService { .filter(result => result.status === 'fulfilled') .map(result => (result as PromiseFulfilledResult>).value); } + + @bindThis + public async packInternal( + src: MiEmoji['id'] | MiEmoji, + ): Promise> { + const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src }); + + return { + id: emoji.id, + createdAt: this.idService.parse(emoji.id).date.toISOString(), + updatedAt: emoji.updatedAt?.toISOString() ?? null, + aliases: emoji.aliases, + name: emoji.name, + category: emoji.category, + host: emoji.host, + // || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) + url: emoji.publicUrl || emoji.originalUrl, + license: emoji.license, + isSensitive: emoji.isSensitive, + localOnly: emoji.localOnly, + requestedBy: emoji.requestedBy, + memo: emoji.memo, + roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, + roleIdsThatCanNotBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction, + }; + } + + @bindThis + public async packInternalMany( + emojis: (MiEmoji['id'] | MiEmoji)[], + ) : Promise[]> { + return (await Promise.allSettled(emojis.map(x => this.packInternal(x)))) + .filter(result => result.status === 'fulfilled') + .map(result => (result as PromiseFulfilledResult>).value); + } } diff --git a/packages/backend/src/models/Emoji.ts b/packages/backend/src/models/Emoji.ts index f05121317e..25d487e221 100644 --- a/packages/backend/src/models/Emoji.ts +++ b/packages/backend/src/models/Emoji.ts @@ -76,6 +76,16 @@ export class MiEmoji { }) public isSensitive: boolean; + @Column('varchar', { + length: 1024, nullable: true, + }) + public requestedBy: string | null; + + @Column('varchar', { + length: 8192, default: '', + }) + public memo: string | null; + // TODO: 定期ジョブで存在しなくなったロールIDを除去するようにする @Column('varchar', { array: true, length: 128, default: '{}', diff --git a/packages/backend/src/models/json-schema/emoji.ts b/packages/backend/src/models/json-schema/emoji.ts index 158f2bee8b..c6bbfcd292 100644 --- a/packages/backend/src/models/json-schema/emoji.ts +++ b/packages/backend/src/models/json-schema/emoji.ts @@ -60,6 +60,16 @@ export const packedEmojiDetailedSchema = { optional: false, nullable: false, format: 'id', }, + createdAt: { + type: 'string', + optional: true, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: true, nullable: true, + format: 'date-time', + }, aliases: { type: 'array', optional: false, nullable: false, @@ -98,6 +108,14 @@ export const packedEmojiDetailedSchema = { type: 'boolean', optional: false, nullable: false, }, + requestedBy: { + type: 'string', + optional: true, nullable: true, + }, + memo: { + type: 'string', + optional: true, nullable: true, + }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', optional: true, nullable: false, diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 0d9ffe1b01..3d1de97cab 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -102,6 +102,8 @@ export class ImportCustomEmojisProcessorService { license: emojiInfo.license, isSensitive: emojiInfo.isSensitive, localOnly: emojiInfo.localOnly, + requestedBy: emojiInfo.requestedBy, + memo: emojiInfo.memo, roleIdsThatCanBeUsedThisEmojiAsReaction: [], roleIdsThatCanNotBeUsedThisEmojiAsReaction: [], }); 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 7327aafe6e..31bdfad7fb 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -50,6 +50,8 @@ export const paramDef = { license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + requestedBy: { type: 'string', nullable: true }, + memo: { type: 'string', nullable: true }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', format: 'misskey:id', @@ -89,6 +91,8 @@ export default class extends Endpoint { // eslint- license: ps.license ?? null, isSensitive: ps.isSensitive ?? false, localOnly: ps.localOnly ?? false, + requestedBy: ps.requestedBy ?? null, + memo: ps.memo ?? null, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [], roleIdsThatCanNotBeUsedThisEmojiAsReaction: ps.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? [], }, me); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index 2dfa95d303..5a5579f101 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -106,6 +106,8 @@ export default class extends Endpoint { // eslint- license: emoji.license, isSensitive: emoji.isSensitive, localOnly: emoji.localOnly, + requestedBy: emoji.requestedBy, + memo: emoji.memo, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction, roleIdsThatCanNotBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction, }, me); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index f3e0c1ef1f..7d4a9096fb 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -106,7 +106,7 @@ export default class extends Endpoint { // eslint- .limit(ps.limit) .getMany(); - return this.emojiEntityService.packDetailedMany(emojis); + return this.emojiEntityService.packInternalMany(emojis); }); } } 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 59e87253f6..f51cc1208a 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -10,7 +10,6 @@ import type { MiEmoji } from '@/models/Emoji.js'; import { QueryService } from '@/core/QueryService.js'; import { DI } from '@/di-symbols.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; -//import { sqlLikeEscape } from '@/misc/sql-like-escape.js'; export const meta = { tags: ['admin'], @@ -88,28 +87,26 @@ export default class extends Endpoint { // eslint- let emojis: MiEmoji[]; if (ps.query) { - //q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` }); - //const emojis = await q.limit(ps.limit).getMany(); + if (ps.query.startsWith(':') && ps.query.endsWith(':') && ps.query.length > 2) { + // 登録名と完全一致の検索 + q.andWhere('emoji.name = :q', { q: ps.query.slice(1, -1) }); - emojis = await q.getMany(); - const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g); - - if (queryarry) { - emojis = emojis.filter(emoji => - queryarry.includes(`:${emoji.name}:`), - ); + emojis = await q.limit(ps.limit).getMany(); } else { - emojis = emojis.filter(emoji => - emoji.name.includes(ps.query!) || - emoji.aliases.some(a => a.includes(ps.query!)) || - emoji.category?.includes(ps.query!)); + // 登録名、エイリアス、カテゴリーの部分一致の検索 + // TODO: クエリーで処理したいが、aliasesがarrayなので複雑になりすぎるためいったん放置 + emojis = (await q.getMany()) + .filter(emoji => + emoji.name.includes(ps.query!) || + emoji.aliases.some(a => a.includes(ps.query!)) || + emoji.category?.includes(ps.query!)) + .splice(ps.limit + 1); } - emojis.splice(ps.limit + 1); } else { emojis = await q.limit(ps.limit).getMany(); } - return this.emojiEntityService.packDetailedMany(emojis); + return this.emojiEntityService.packInternalMany(emojis); }); } } 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 cf44ccd10f..11f94c67e4 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -53,6 +53,8 @@ export const paramDef = { license: { type: 'string', nullable: true }, isSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + requestedBy: { type: 'string', nullable: true }, + memo: { type: 'string', nullable: true }, roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: { type: 'string', format: 'misskey:id', @@ -98,6 +100,8 @@ export default class extends Endpoint { // eslint- license: ps.license ?? null, isSensitive: ps.isSensitive, localOnly: ps.localOnly, + requestedBy: ps.requestedBy ?? null, + memo: ps.memo ?? null, roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction, roleIdsThatCanNotBeUsedThisEmojiAsReaction: ps.roleIdsThatCanNotBeUsedThisEmojiAsReaction, }, me); diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index f74fa1175e..bb14af7638 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -224,7 +224,8 @@ const setCategoryBulk = async () => { ids: selectedEmojis.value, category: result, }); - emojisPaginationComponent.value.reload(); + selectedEmojis.value = []; + emojisPaginationComponent.value?.reload(); }; const setLicenseBulk = async () => { @@ -237,7 +238,8 @@ const setLicenseBulk = async () => { ids: selectedEmojis.value, license: result, }); - emojisPaginationComponent.value.reload(); + selectedEmojis.value = []; + emojisPaginationComponent.value?.reload(); }; const addTagBulk = async () => { @@ -249,7 +251,8 @@ const addTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value.reload(); + selectedEmojis.value = []; + emojisPaginationComponent.value?.reload(); }; const removeTagBulk = async () => { @@ -261,7 +264,8 @@ const removeTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value.reload(); + selectedEmojis.value = []; + emojisPaginationComponent.value?.reload(); }; const setTagBulk = async () => { @@ -273,7 +277,8 @@ const setTagBulk = async () => { ids: selectedEmojis.value, aliases: result.split(' '), }); - emojisPaginationComponent.value.reload(); + selectedEmojis.value = []; + emojisPaginationComponent.value?.reload(); }; const delBulk = async () => { @@ -285,7 +290,8 @@ const delBulk = async () => { await os.apiWithDialog('admin/emoji/delete-bulk', { ids: selectedEmojis.value, }); - emojisPaginationComponent.value.reload(); + selectedEmojis.value = []; + emojisPaginationComponent.value?.reload(); }; const headerActions = computed(() => [{ diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 6f14a91baa..262dbfec93 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -49,6 +49,20 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + + + + + + @@ -111,6 +125,7 @@ import { customEmojiCategories } from '@/custom-emojis.js'; import MkSwitch from '@/components/MkSwitch.vue'; import { selectFile } from '@/scripts/select-file.js'; import MkRolePreview from '@/components/MkRolePreview.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; const props = defineProps<{ emoji?: any, @@ -120,9 +135,13 @@ const windowEl = ref | null>(null); const name = ref(props.emoji ? props.emoji.name : ''); const category = ref(props.emoji ? props.emoji.category : ''); const aliases = ref(props.emoji ? props.emoji.aliases.join(' ') : ''); +const createdAt = ref(props.emoji ? props.emoji.createdAt : null); +const updatedAt = ref(props.emoji ? props.emoji.updatedAt : null); const license = ref(props.emoji ? (props.emoji.license ?? '') : ''); const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false); const localOnly = ref(props.emoji ? props.emoji.localOnly : false); +const requestedBy = ref(props.emoji ? props.emoji.requestedBy : ''); +const memo = ref(props.emoji ? props.emoji.memo : ''); const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); const rolesThatCanBeUsedThisEmojiAsReaction = ref([]); const roleIdsThatCanNotBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction : []); @@ -178,6 +197,8 @@ async function done() { license: license.value === '' ? null : license.value, isSensitive: isSensitive.value, localOnly: localOnly.value, + requestedBy: requestedBy.value, + memo: memo.value, roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id), roleIdsThatCanNotBeUsedThisEmojiAsReaction: rolesThatCanNotBeUsedThisEmojiAsReaction.value.map(x => x.id), }; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index c6c9cc70b7..5774553c3f 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4527,6 +4527,10 @@ export type components = { EmojiDetailed: { /** Format: id */ id: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string | null; aliases: string[]; name: string; category: string | null; @@ -4536,6 +4540,8 @@ export type components = { license: string | null; isSensitive: boolean; localOnly: boolean; + requestedBy?: string | null; + memo?: string | null; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; }; @@ -6682,6 +6688,8 @@ export type operations = { license?: string | null; isSensitive?: boolean; localOnly?: boolean; + requestedBy?: string | null; + memo?: string | null; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; }; @@ -7311,6 +7319,8 @@ export type operations = { license?: string | null; isSensitive?: boolean; localOnly?: boolean; + requestedBy?: string | null; + memo?: string | null; roleIdsThatCanBeUsedThisEmojiAsReaction?: string[]; roleIdsThatCanNotBeUsedThisEmojiAsReaction?: string[]; };