Feat:絵文字申請機能の追加
This commit is contained in:
parent
1966876320
commit
08311ece41
|
@ -978,6 +978,7 @@ export interface Locale {
|
||||||
"unassign": string;
|
"unassign": string;
|
||||||
"color": string;
|
"color": string;
|
||||||
"manageCustomEmojis": string;
|
"manageCustomEmojis": string;
|
||||||
|
"requestCustomEmojis": string;
|
||||||
"youCannotCreateAnymore": string;
|
"youCannotCreateAnymore": string;
|
||||||
"cannotPerformTemporary": string;
|
"cannotPerformTemporary": string;
|
||||||
"cannotPerformTemporaryDescription": string;
|
"cannotPerformTemporaryDescription": string;
|
||||||
|
@ -1020,6 +1021,7 @@ export interface Locale {
|
||||||
"sensitiveWordsDescription2": string;
|
"sensitiveWordsDescription2": string;
|
||||||
"notesSearchNotAvailable": string;
|
"notesSearchNotAvailable": string;
|
||||||
"license": string;
|
"license": string;
|
||||||
|
"draft": string;
|
||||||
"unfavoriteConfirm": string;
|
"unfavoriteConfirm": string;
|
||||||
"myClips": string;
|
"myClips": string;
|
||||||
"drivecleaner": string;
|
"drivecleaner": string;
|
||||||
|
@ -1556,6 +1558,7 @@ export interface Locale {
|
||||||
"inviteLimitCycle": string;
|
"inviteLimitCycle": string;
|
||||||
"inviteExpirationTime": string;
|
"inviteExpirationTime": string;
|
||||||
"canManageCustomEmojis": string;
|
"canManageCustomEmojis": string;
|
||||||
|
"canRequestCustomEmojis": string;
|
||||||
"driveCapacity": string;
|
"driveCapacity": string;
|
||||||
"alwaysMarkNsfw": string;
|
"alwaysMarkNsfw": string;
|
||||||
"pinMax": string;
|
"pinMax": string;
|
||||||
|
|
|
@ -975,6 +975,7 @@ assign: "アサイン"
|
||||||
unassign: "アサインを解除"
|
unassign: "アサインを解除"
|
||||||
color: "色"
|
color: "色"
|
||||||
manageCustomEmojis: "カスタム絵文字の管理"
|
manageCustomEmojis: "カスタム絵文字の管理"
|
||||||
|
requestCustomEmojis: "カスタム絵文字のリクエスト"
|
||||||
youCannotCreateAnymore: "これ以上作成することはできません。"
|
youCannotCreateAnymore: "これ以上作成することはできません。"
|
||||||
cannotPerformTemporary: "一時的に利用できません"
|
cannotPerformTemporary: "一時的に利用できません"
|
||||||
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
|
||||||
|
@ -1017,6 +1018,7 @@ sensitiveWordsDescription: "設定したワードが含まれるノートの公
|
||||||
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
sensitiveWordsDescription2: "スペースで区切るとAND指定になり、キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
|
draft: "ドラフト"
|
||||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||||
myClips: "自分のクリップ"
|
myClips: "自分のクリップ"
|
||||||
drivecleaner: "ドライブクリーナー"
|
drivecleaner: "ドライブクリーナー"
|
||||||
|
@ -1477,6 +1479,7 @@ _role:
|
||||||
inviteLimitCycle: "招待コードの発行間隔"
|
inviteLimitCycle: "招待コードの発行間隔"
|
||||||
inviteExpirationTime: "招待コードの有効期限"
|
inviteExpirationTime: "招待コードの有効期限"
|
||||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||||
|
canRequestCustomEmojis: "カスタム絵文字のリクエスト"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,10 +13,10 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||||
import type { MiEmoji } from '@/models/Emoji.js';
|
import type { MiEmoji } from '@/models/Emoji.js';
|
||||||
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
||||||
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { query } from '@/misc/prelude/url.js';
|
|
||||||
import type { Serialized } from '@/types.js';
|
import type { Serialized } from '@/types.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
@Inject(DI.driveFilesRepository)
|
||||||
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private emojiEntityService: EmojiEntityService,
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
@ -66,6 +69,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
license: string | null;
|
license: string | null;
|
||||||
isSensitive: boolean;
|
isSensitive: boolean;
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
|
draft: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
||||||
}, moderator?: MiUser): Promise<MiEmoji> {
|
}, moderator?: MiUser): Promise<MiEmoji> {
|
||||||
const emoji = await this.emojisRepository.insert({
|
const emoji = await this.emojisRepository.insert({
|
||||||
|
@ -82,6 +86,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
isSensitive: data.isSensitive,
|
isSensitive: data.isSensitive,
|
||||||
localOnly: data.localOnly,
|
localOnly: data.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
|
draft: data.draft,
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
if (data.host == null) {
|
if (data.host == null) {
|
||||||
|
@ -109,14 +114,18 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
license?: string | null;
|
license?: string | null;
|
||||||
|
fileId?: string | null;
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
localOnly?: boolean;
|
localOnly?: boolean;
|
||||||
|
draft: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
||||||
}, moderator?: MiUser): Promise<void> {
|
}, moderator?: MiUser): Promise<void> {
|
||||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
|
const driveFile = data.fileId !== null ? await this.driveFilesRepository.findOneBy({ id: data.fileId }) : null;
|
||||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||||
|
|
||||||
|
if (driveFile !== null) {
|
||||||
await this.emojisRepository.update(emoji.id, {
|
await this.emojisRepository.update(emoji.id, {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
@ -128,8 +137,21 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
||||||
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
|
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
|
||||||
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? 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,
|
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
|
||||||
|
draft: data.draft,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.emojisRepository.update(emoji.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: data.name,
|
||||||
|
category: data.category,
|
||||||
|
aliases: data.aliases,
|
||||||
|
license: data.license,
|
||||||
|
isSensitive: data.isSensitive,
|
||||||
|
localOnly: data.localOnly,
|
||||||
|
draft: data.draft,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction ?? undefined,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.localEmojisCache.refresh();
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type RolePolicies = {
|
||||||
inviteLimitCycle: number;
|
inviteLimitCycle: number;
|
||||||
inviteExpirationTime: number;
|
inviteExpirationTime: number;
|
||||||
canManageCustomEmojis: boolean;
|
canManageCustomEmojis: boolean;
|
||||||
|
canRequestCustomEmojis: boolean;
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
canUseTranslator: boolean;
|
canUseTranslator: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
|
@ -57,6 +58,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
inviteLimitCycle: 60 * 24 * 7,
|
inviteLimitCycle: 60 * 24 * 7,
|
||||||
inviteExpirationTime: 0,
|
inviteExpirationTime: 0,
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
|
canRequestCustomEmojis: false,
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
canUseTranslator: true,
|
canUseTranslator: true,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
|
@ -300,6 +302,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
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)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -33,6 +33,7 @@ export class EmojiEntityService {
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: emoji.publicUrl || emoji.originalUrl,
|
||||||
isSensitive: emoji.isSensitive ? true : undefined,
|
isSensitive: emoji.isSensitive ? true : undefined,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||||
|
draft: emoji.draft,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +62,7 @@ export class EmojiEntityService {
|
||||||
isSensitive: emoji.isSensitive,
|
isSensitive: emoji.isSensitive,
|
||||||
localOnly: emoji.localOnly,
|
localOnly: emoji.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
|
draft: emoji.draft,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,4 +81,10 @@ export class MiEmoji {
|
||||||
array: true, length: 128, default: '{}',
|
array: true, length: 128, default: '{}',
|
||||||
})
|
})
|
||||||
public roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
|
public roleIdsThatCanBeUsedThisEmojiAsReaction: string[];
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
public draft: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,10 @@ export const packedEmojiSimpleSchema = {
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
draft: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -81,6 +85,10 @@ export const packedEmojiDetailedSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
draft: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
isSensitive: {
|
isSensitive: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -103,6 +103,7 @@ export class ImportCustomEmojisProcessorService {
|
||||||
isSensitive: emojiInfo.isSensitive,
|
isSensitive: emojiInfo.isSensitive,
|
||||||
localOnly: emojiInfo.localOnly,
|
localOnly: emojiInfo.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
||||||
|
draft: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,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_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_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_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_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_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
|
||||||
import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
|
import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
|
||||||
|
@ -375,6 +376,7 @@ const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass
|
||||||
const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default };
|
const $admin_drive_showFile: Provider = { provide: 'ep:admin/drive/show-file', useClass: ep___admin_drive_showFile.default };
|
||||||
const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default };
|
const $admin_emoji_addAliasesBulk: Provider = { provide: 'ep:admin/emoji/add-aliases-bulk', useClass: ep___admin_emoji_addAliasesBulk.default };
|
||||||
const $admin_emoji_add: Provider = { provide: 'ep:admin/emoji/add', useClass: ep___admin_emoji_add.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_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_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 };
|
const $admin_emoji_delete: Provider = { provide: 'ep:admin/emoji/delete', useClass: ep___admin_emoji_delete.default };
|
||||||
|
@ -729,6 +731,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_drive_showFile,
|
$admin_drive_showFile,
|
||||||
$admin_emoji_addAliasesBulk,
|
$admin_emoji_addAliasesBulk,
|
||||||
$admin_emoji_add,
|
$admin_emoji_add,
|
||||||
|
$admin_emoji_addDraft,
|
||||||
$admin_emoji_copy,
|
$admin_emoji_copy,
|
||||||
$admin_emoji_deleteBulk,
|
$admin_emoji_deleteBulk,
|
||||||
$admin_emoji_delete,
|
$admin_emoji_delete,
|
||||||
|
@ -1077,6 +1080,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_drive_showFile,
|
$admin_drive_showFile,
|
||||||
$admin_emoji_addAliasesBulk,
|
$admin_emoji_addAliasesBulk,
|
||||||
$admin_emoji_add,
|
$admin_emoji_add,
|
||||||
|
$admin_emoji_addDraft,
|
||||||
$admin_emoji_copy,
|
$admin_emoji_copy,
|
||||||
$admin_emoji_deleteBulk,
|
$admin_emoji_deleteBulk,
|
||||||
$admin_emoji_delete,
|
$admin_emoji_delete,
|
||||||
|
|
|
@ -25,6 +25,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_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_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_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_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_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
|
||||||
import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
|
import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
|
||||||
|
@ -373,6 +374,7 @@ const eps = [
|
||||||
['admin/drive/show-file', ep___admin_drive_showFile],
|
['admin/drive/show-file', ep___admin_drive_showFile],
|
||||||
['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk],
|
['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk],
|
||||||
['admin/emoji/add', ep___admin_emoji_add],
|
['admin/emoji/add', ep___admin_emoji_add],
|
||||||
|
['admin/emoji/add-draft', ep___admin_emoji_addDraft],
|
||||||
['admin/emoji/copy', ep___admin_emoji_copy],
|
['admin/emoji/copy', ep___admin_emoji_copy],
|
||||||
['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk],
|
['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk],
|
||||||
['admin/emoji/delete', ep___admin_emoji_delete],
|
['admin/emoji/delete', ep___admin_emoji_delete],
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import rndstr from 'rndstr';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { DriveFilesRepository } from '@/models/index.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' },
|
||||||
|
},
|
||||||
|
required: ['name', 'fileId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
||||||
|
emojiId: emoji.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: emoji.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,7 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
},
|
},
|
||||||
required: ['name', 'fileId'],
|
required: ['name','fileId', 'draft'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// TODO: ロジックをサービスに切り出す
|
// TODO: ロジックをサービスに切り出す
|
||||||
|
@ -68,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
||||||
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
|
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
|
||||||
|
@ -81,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
license: ps.license ?? null,
|
license: ps.license ?? null,
|
||||||
isSensitive: ps.isSensitive ?? false,
|
isSensitive: ps.isSensitive ?? false,
|
||||||
localOnly: ps.localOnly ?? false,
|
localOnly: ps.localOnly ?? false,
|
||||||
|
draft: false,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
|
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,9 @@ export const paramDef = {
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
draft: { type: 'boolean' },
|
||||||
},
|
},
|
||||||
required: ['id', 'name', 'aliases'],
|
required: ['id', 'name', 'aliases', 'draft'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -93,6 +94,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
isSensitive: ps.isSensitive,
|
isSensitive: ps.isSensitive,
|
||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
|
fileId: ps.fileId ?? null,
|
||||||
|
draft: ps.draft,
|
||||||
}, me);
|
}, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,10 @@ const emojiDb = computed(() => {
|
||||||
const customEmojiDB: EmojiDef[] = [];
|
const customEmojiDB: EmojiDef[] = [];
|
||||||
|
|
||||||
for (const x of customEmojis.value) {
|
for (const x of customEmojis.value) {
|
||||||
|
if (x.draft) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
customEmojiDB.push({
|
customEmojiDB.push({
|
||||||
name: x.name,
|
name: x.name,
|
||||||
emoji: `:${x.name}:`,
|
emoji: `:${x.name}:`,
|
||||||
|
|
|
@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
v-for="category in customEmojiCategories"
|
v-for="category in customEmojiCategories"
|
||||||
:key="`custom:${category}`"
|
:key="`custom:${category}`"
|
||||||
:initialShown="false"
|
:initialShown="false"
|
||||||
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
:emojis="computed(() => customEmojis.filter(emoji => !emoji.draft).filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
|
||||||
@chosen="chosen"
|
@chosen="chosen"
|
||||||
>
|
>
|
||||||
{{ category || i18n.ts.other }}
|
{{ category || i18n.ts.other }}
|
||||||
|
@ -157,7 +157,7 @@ watch(q, () => {
|
||||||
|
|
||||||
const searchCustom = () => {
|
const searchCustom = () => {
|
||||||
const max = 100;
|
const max = 100;
|
||||||
const emojis = customEmojis.value;
|
const emojis = customEmojis.value.filter(emoji => !emoji.draft);
|
||||||
const matches = new Set<Misskey.entities.CustomEmoji>();
|
const matches = new Set<Misskey.entities.CustomEmoji>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
||||||
|
|
|
@ -66,6 +66,7 @@ export const ROLE_POLICIES = [
|
||||||
'inviteLimitCycle',
|
'inviteLimitCycle',
|
||||||
'inviteExpirationTime',
|
'inviteExpirationTime',
|
||||||
'canManageCustomEmojis',
|
'canManageCustomEmojis',
|
||||||
|
'canRequestCustomEmojis',
|
||||||
'canSearchNotes',
|
'canSearchNotes',
|
||||||
'canUseTranslator',
|
'canUseTranslator',
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
|
|
|
@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
<MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton>
|
<MkButton v-if="$i && ($i.isModerator || $i.policies.canManageCustomEmojis)" primary link to="/custom-emojis-manager">{{ i18n.ts.manageCustomEmojis }}</MkButton>
|
||||||
|
<MkButton v-if="$i && (!$i.isModerator && !$i.policies.canManageCustomEmojis && $i.policies.canRequestCustomEmojis)" primary @click="edit">{{ i18n.ts.requestCustomEmojis }}</MkButton>
|
||||||
|
|
||||||
<div class="query">
|
<div class="query">
|
||||||
<MkInput v-model="q" class="" :placeholder="i18n.ts.search">
|
<MkInput v-model="q" class="" :placeholder="i18n.ts.search">
|
||||||
|
@ -22,21 +23,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFoldableSection v-if="searchEmojis">
|
<MkFoldableSection v-if="searchEmojis">
|
||||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||||
<div :class="$style.emojis">
|
<div :class="$style.emojis">
|
||||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji"/>
|
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji" :draft="emoji.draft"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
|
|
||||||
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category">
|
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category">
|
||||||
<template #header>{{ category || i18n.ts.other }}</template>
|
<template #header>{{ category || i18n.ts.other }}</template>
|
||||||
<div :class="$style.emojis">
|
<div :class="$style.emojis">
|
||||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/>
|
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji" :draft="emoji.draft"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch } from 'vue';
|
import { watch, defineAsyncComponent } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XEmoji from './emojis.emoji.vue';
|
import XEmoji from './emojis.emoji.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -44,6 +45,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
|
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import * as os from '@/os';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const customEmojiTags = getCustomEmojiTags();
|
const customEmojiTags = getCustomEmojiTags();
|
||||||
|
@ -80,6 +82,24 @@ function toggleTag(tag) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const edit = () => {
|
||||||
|
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
||||||
|
emoji: {
|
||||||
|
name: '',
|
||||||
|
category: null,
|
||||||
|
aliases: [],
|
||||||
|
license: '',
|
||||||
|
url: '',
|
||||||
|
draft: true,
|
||||||
|
},
|
||||||
|
isRequest: true,
|
||||||
|
}, {
|
||||||
|
done: result => {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
}, 'closed');
|
||||||
|
};
|
||||||
|
|
||||||
watch($$(q), () => {
|
watch($$(q), () => {
|
||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
|
|
@ -259,6 +259,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canRequestCustomEmojis, 'canRequestCustomEmojis'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canRequestCustomEmojis }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.canRequestCustomEmojis.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.canRequestCustomEmojis.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canRequestCustomEmojis)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.canRequestCustomEmojis.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.canRequestCustomEmojis.value" :disabled="role.policies.canRequestCustomEmojis.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.canRequestCustomEmojis.priority" :min="0" :max="2" :step="1" easing :text-converter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -87,6 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canRequestCustomEmojis, 'canRequestCustomEmojis'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canRequestCustomEmojis }}</template>
|
||||||
|
<template #suffix>{{ policies.canRequestCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<MkSwitch v-model="policies.canRequestCustomEmojis">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canSearchNotes, 'canSearchNotes'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
<template #label>{{ i18n.ts._role._options.canSearchNotes }}</template>
|
||||||
<template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
|
|
@ -30,14 +30,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||||
<template #default="{items}">
|
<template #default="{items}">
|
||||||
<div class="ldhfsamy">
|
<div class="ldhfsamy">
|
||||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
<div v-for="emoji in items" :key="emoji.id">
|
||||||
|
<button v-if="emoji.draft" class="emoji _panel _button emoji-draft" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||||
<img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/>
|
<img :src="`/emoji/${emoji.name}.webp`" class="img" :alt="emoji.name"/>
|
||||||
|
<div class="body">
|
||||||
|
<div class="name _monospace">{{ emoji.name + ' (draft)' }}</div>
|
||||||
|
<div class="info">{{ emoji.category }}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button v-else class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||||
|
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name _monospace">{{ emoji.name }}</div>
|
<div class="name _monospace">{{ emoji.name }}</div>
|
||||||
<div class="info">{{ emoji.category }}</div>
|
<div class="info">{{ emoji.category }}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MkPagination>
|
</MkPagination>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #default="{items}">
|
<template #default="{items}">
|
||||||
<div class="ldhfsamy">
|
<div class="ldhfsamy">
|
||||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||||
<img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" class="img" :alt="emoji.name"/>
|
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name _monospace">{{ emoji.name }}</div>
|
<div class="name _monospace">{{ emoji.name }}</div>
|
||||||
<div class="info">{{ emoji.host }}</div>
|
<div class="info">{{ emoji.host }}</div>
|
||||||
|
@ -141,6 +150,7 @@ const add = async (ev: MouseEvent) => {
|
||||||
const edit = (emoji) => {
|
const edit = (emoji) => {
|
||||||
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
|
isRequest: false,
|
||||||
}, {
|
}, {
|
||||||
done: result => {
|
done: result => {
|
||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
|
@ -323,12 +333,13 @@ definePageMetadata(computed(() => ({
|
||||||
grid-gap: 12px;
|
grid-gap: 12px;
|
||||||
margin: var(--margin) 0;
|
margin: var(--margin) 0;
|
||||||
|
|
||||||
> .emoji {
|
div > .emoji {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 11px;
|
padding: 11px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: solid 1px var(--panel);
|
border: solid 1px var(--panel);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--inputBorderHover);
|
border-color: var(--inputBorderHover);
|
||||||
|
@ -410,4 +421,10 @@ definePageMetadata(computed(() => ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-draft {
|
||||||
|
--c: rgb(255 196 0 / 15%);;
|
||||||
|
background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkModalWindow
|
<MkModalWindow
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="400"
|
:width="400"
|
||||||
|
:withOkButton="true"
|
||||||
@close="dialog.close()"
|
@close="dialog.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
|
@ -68,6 +69,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
<div :class="$style.footer">
|
<div :class="$style.footer">
|
||||||
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
|
||||||
|
<MkSwitch v-if="!isRequest" v-model="draft" :disabled="isRequest">
|
||||||
|
{{ i18n.ts.draft }}
|
||||||
|
</MkSwitch>
|
||||||
|
<MkButton v-if="!isRequest" danger @click="del()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MkModalWindow>
|
</MkModalWindow>
|
||||||
|
@ -90,6 +95,7 @@ import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emoji?: any,
|
emoji?: any,
|
||||||
|
isRequest: boolean,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let dialog = $ref(null);
|
let dialog = $ref(null);
|
||||||
|
@ -102,18 +108,55 @@ let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
|
||||||
let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||||
let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
|
let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
|
||||||
let file = $ref<Misskey.entities.DriveFile>();
|
let file = $ref<Misskey.entities.DriveFile>();
|
||||||
|
let chooseFile: DriveFile|null = $ref(null);
|
||||||
|
let draft = $ref(props.emoji.draft);
|
||||||
|
let isRequest = $ref(props.isRequest);
|
||||||
|
|
||||||
watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
|
watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
|
||||||
rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
const imgUrl = computed(() => file ? file.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
||||||
|
let draft = $ref(props.emoji.draft);
|
||||||
|
let isRequest = $ref(props.isRequest);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||||
(ev: 'closed'): void
|
(ev: 'closed'): void
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
function ok() {
|
||||||
|
if (isRequest) {
|
||||||
|
if (chooseFile !== null && name.match(/^[a-zA-Z0-9_]+$/)) {
|
||||||
|
add();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add() {
|
||||||
|
const ret = await os.api('admin/emoji/add-draft', {
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
aliases: aliases.split(' '),
|
||||||
|
license: license === '' ? null : license,
|
||||||
|
fileId: chooseFile.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('done', {
|
||||||
|
updated: {
|
||||||
|
id: ret.id,
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
aliases: aliases.split(' '),
|
||||||
|
license: license === '' ? null : license,
|
||||||
|
draft: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
async function changeImage(ev) {
|
async function changeImage(ev) {
|
||||||
file = await selectFile(ev.currentTarget ?? ev.target, null);
|
file = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||||
const candidate = file.name.replace(/\.(.+)$/, '');
|
const candidate = file.name.replace(/\.(.+)$/, '');
|
||||||
|
@ -137,7 +180,30 @@ async function addRole() {
|
||||||
async function removeRole(role, ev) {
|
async function removeRole(role, ev) {
|
||||||
rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id);
|
rolesThatCanBeUsedThisEmojiAsReaction = rolesThatCanBeUsedThisEmojiAsReaction.filter(x => x.id !== role.id);
|
||||||
}
|
}
|
||||||
|
async function update() {
|
||||||
|
await os.apiWithDialog('admin/emoji/update', {
|
||||||
|
id: props.emoji.id,
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
aliases: aliases.split(' '),
|
||||||
|
license: license === '' ? null : license,
|
||||||
|
fileId: chooseFile?.id,
|
||||||
|
draft: draft,
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('done', {
|
||||||
|
updated: {
|
||||||
|
id: props.emoji.id,
|
||||||
|
name,
|
||||||
|
category,
|
||||||
|
aliases: aliases.split(' '),
|
||||||
|
license: license === '' ? null : license,
|
||||||
|
draft: draft,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.close();
|
||||||
|
}
|
||||||
async function done() {
|
async function done() {
|
||||||
const params = {
|
const params = {
|
||||||
name,
|
name,
|
||||||
|
@ -178,6 +244,13 @@ async function done() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseFileFrom(ev) {
|
||||||
|
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
|
||||||
|
chooseFile = files_[0];
|
||||||
|
url = chooseFile.url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function del() {
|
async function del() {
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -4,7 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button class="_button" :class="$style.root" @click="menu">
|
<button v-if="emoji.draft" class="zuvgdzyu _button emoji-draft" @click="menu">
|
||||||
|
<img :src="emoji.url" class="img" loading="lazy"/>
|
||||||
|
<div class="body">
|
||||||
|
<div class="name _monospace">{{ emoji.name + ' (draft)' }}</div>
|
||||||
|
<div class="info">{{ emoji.aliases.join(' ') }}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button v-else class="_button" :class="$style.root" @click="menu">
|
||||||
<img :src="emoji.url" :class="$style.img" loading="lazy"/>
|
<img :src="emoji.url" :class="$style.img" loading="lazy"/>
|
||||||
<div :class="$style.body">
|
<div :class="$style.body">
|
||||||
<div :class="$style.name" class="_monospace">{{ emoji.name }}</div>
|
<div :class="$style.name" class="_monospace">{{ emoji.name }}</div>
|
||||||
|
@ -25,6 +32,7 @@ const props = defineProps<{
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
category: string;
|
category: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
draft: boolean;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -91,4 +99,10 @@ function menu(ev) {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-draft {
|
||||||
|
--c: rgb(255 196 0 / 15%);;
|
||||||
|
background-image: linear-gradient(45deg,var(--c) 16.67%,transparent 16.67%,transparent 50%,var(--c) 50%,var(--c) 66.67%,transparent 66.67%,transparent 100%);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -311,6 +311,7 @@ export type CustomEmoji = {
|
||||||
url: string;
|
url: string;
|
||||||
category: string;
|
category: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
|
draft: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LiteInstanceMetadata = {
|
export type LiteInstanceMetadata = {
|
||||||
|
|
Loading…
Reference in New Issue