Merge remote-tracking branch 'misskey-mattyatea/emoji-request' into develop
# Conflicts: # CHANGELOG.md # locales/index.d.ts # locales/ja-JP.yml # packages/backend/src/core/CustomEmojiService.ts # packages/backend/src/models/RepositoryModule.ts # packages/backend/src/server/api/EndpointsModule.ts # packages/backend/src/server/api/endpoints.ts # packages/backend/src/server/api/endpoints/admin/emoji/update.ts # packages/frontend/src/components/MkCustomEmojiEditLocal.vue # packages/frontend/src/components/MkCustomEmojiEditRemote.vue # packages/frontend/src/components/MkEmojiEditDialog.vue # packages/frontend/src/pages/about.emojis.vue # packages/frontend/src/pages/admin/roles.editor.vue # packages/frontend/src/pages/custom-emojis-manager.vue # packages/frontend/src/pages/emojis.emoji.vue
This commit is contained in:
commit
68b48bc16f
|
@ -37,7 +37,6 @@
|
|||
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
|
||||
- Enhance: ローカリゼーションの更新
|
||||
- Enhance: 依存関係の更新
|
||||
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
||||
|
||||
### Client
|
||||
- Feat: 今日誕生日のフォロー中のユーザーを一覧表示できるウィジェットを追加
|
||||
|
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
|
@ -279,6 +279,7 @@ export interface Locale {
|
|||
"imageUrl": string;
|
||||
"remove": string;
|
||||
"removed": string;
|
||||
"requestApprovalAreYouSure": string;
|
||||
"removeAreYouSure": string;
|
||||
"deleteAreYouSure": string;
|
||||
"undraftAreYouSure": string;
|
||||
|
@ -869,9 +870,9 @@ export interface Locale {
|
|||
"high": string;
|
||||
"middle": string;
|
||||
"low": string;
|
||||
"list": string;
|
||||
"GamingSpeedChange": string;
|
||||
"GamingSpeedChangeInfo": string;
|
||||
"list": string;
|
||||
"emailNotConfiguredWarning": string;
|
||||
"ratio": string;
|
||||
"showVisibilityColor": string;
|
||||
|
@ -1077,6 +1078,9 @@ export interface Locale {
|
|||
"license": string;
|
||||
"draft": string;
|
||||
"undrafted": string;
|
||||
"requestPending": string;
|
||||
"approval": string;
|
||||
"requestingEmojis": string;
|
||||
"unfavoriteConfirm": string;
|
||||
"myClips": string;
|
||||
"drivecleaner": string;
|
||||
|
|
|
@ -276,6 +276,7 @@ announcements: "お知らせ"
|
|||
imageUrl: "画像URL"
|
||||
remove: "削除"
|
||||
removed: "削除しました"
|
||||
requestApprovalAreYouSure: "「{x}」のリクエストを承認しますか?"
|
||||
removeAreYouSure: "「{x}」を削除しますか?"
|
||||
deleteAreYouSure: "「{x}」を削除しますか?"
|
||||
undraftAreYouSure: "「{x}」をドラフト解除しますか?"
|
||||
|
@ -866,9 +867,9 @@ priority: "優先度"
|
|||
high: "高"
|
||||
middle: "中"
|
||||
low: "低"
|
||||
list: "一覧"
|
||||
GamingSpeedChange: "ゲーミングの光るスピードの調整"
|
||||
GamingSpeedChangeInfo: "左にすれば早くなる、右にすれば遅くなる。それだけ。"
|
||||
list: "一覧"
|
||||
emailNotConfiguredWarning: "メールアドレスの設定がされていません。"
|
||||
ratio: "比率"
|
||||
showVisibilityColor: "ノートの公開範囲を色付けする"
|
||||
|
@ -1072,6 +1073,9 @@ hiddenTags: "非表示ハッシュタグ"
|
|||
hiddenTagsDescription: "設定したタグをトレンドに表示させないようにします。改行で区切って複数設定できます。"
|
||||
notesSearchNotAvailable: "ノート検索は利用できません。"
|
||||
license: "ライセンス"
|
||||
requestPending: "申請中"
|
||||
approval: "承認"
|
||||
requestingEmojis: "リクエストされている絵文字"
|
||||
draft: "ドラフト"
|
||||
undrafted: "ドラフト解除"
|
||||
unfavoriteConfirm: "お気に入り解除しますか?"
|
||||
|
|
13
packages/backend/migration/1698131657286-EmojiRequest.js
Normal file
13
packages/backend/migration/1698131657286-EmojiRequest.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export class EmojiRequest1698131657286 {
|
||||
name = 'EmojiRequest1698131657286'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "emoji_request" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE, "name" character varying(128) NOT NULL, "category" character varying(128), "originalUrl" character varying(512) NOT NULL, "publicUrl" character varying(512) NOT NULL DEFAULT '', "type" character varying(64), "aliases" character varying(128) array NOT NULL DEFAULT '{}', "license" character varying(1024), "fileId" character varying(1024) NOT NULL, "localOnly" boolean NOT NULL DEFAULT false, "isSensitive" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_3c74521e048dc744f0c7eb65f4a" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ea1d771e867e9843300f09d02c" ON "emoji_request" ("name") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_ea1d771e867e9843300f09d02c"`);
|
||||
await queryRunner.query(`DROP TABLE "emoji_request"`);
|
||||
}
|
||||
}
|
|
@ -90,6 +90,7 @@ import { ClipEntityService } from './entities/ClipEntityService.js';
|
|||
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
|
||||
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
|
||||
import { EmojiEntityService } from './entities/EmojiEntityService.js';
|
||||
import { EmojiRequestsEntityService } from './entities/EmojiRequestsEntityService.js';
|
||||
import { FollowingEntityService } from './entities/FollowingEntityService.js';
|
||||
import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js';
|
||||
import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js';
|
||||
|
@ -225,6 +226,7 @@ const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting
|
|||
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
|
||||
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
|
||||
const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useExisting: EmojiEntityService };
|
||||
const $EmojiRequestsEntityService: Provider = { provide: 'EmojiRequestsEntityService', useExisting: EmojiRequestsEntityService };
|
||||
const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useExisting: FollowingEntityService };
|
||||
const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useExisting: FollowRequestEntityService };
|
||||
const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useExisting: GalleryLikeEntityService };
|
||||
|
@ -360,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
EmojiEntityService,
|
||||
EmojiRequestsEntityService,
|
||||
FollowingEntityService,
|
||||
FollowRequestEntityService,
|
||||
GalleryLikeEntityService,
|
||||
|
@ -490,6 +493,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
$EmojiEntityService,
|
||||
$EmojiRequestsEntityService,
|
||||
$FollowingEntityService,
|
||||
$FollowRequestEntityService,
|
||||
$GalleryLikeEntityService,
|
||||
|
@ -620,6 +624,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
EmojiEntityService,
|
||||
EmojiRequestsEntityService,
|
||||
FollowingEntityService,
|
||||
FollowRequestEntityService,
|
||||
GalleryLikeEntityService,
|
||||
|
@ -749,6 +754,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
$EmojiEntityService,
|
||||
$EmojiRequestsEntityService,
|
||||
$FollowingEntityService,
|
||||
$FollowRequestEntityService,
|
||||
$GalleryLikeEntityService,
|
||||
|
|
|
@ -12,12 +12,14 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
||||
import type { EmojisRepository, EmojiRequestsRepository, MiRole, MiUser } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { Serialized } from '@/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
|
||||
|
||||
@Injectable()
|
||||
|
@ -32,6 +34,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@Inject(DI.emojiRequestsRepository)
|
||||
private emojiRequestsRepository: EmojiRequestsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
|
@ -54,6 +59,41 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async request(data: {
|
||||
driveFile: MiDriveFile;
|
||||
name: string;
|
||||
category: string | null;
|
||||
aliases: string[];
|
||||
license: string | null;
|
||||
isSensitive: boolean;
|
||||
localOnly: boolean;
|
||||
}, me?: MiUser): Promise<MiEmojiRequest> {
|
||||
const emoji = await this.emojiRequestsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
originalUrl: data.driveFile.url,
|
||||
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
fileId: data.driveFile.id,
|
||||
}).then(x => this.emojiRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (me) {
|
||||
this.moderationLogService.log(me, 'addCustomEmoji', {
|
||||
emojiId: emoji.id,
|
||||
emoji: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async add(data: {
|
||||
driveFile: MiDriveFile;
|
||||
|
@ -64,7 +104,6 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
license: string | null;
|
||||
isSensitive: boolean;
|
||||
localOnly: boolean;
|
||||
draft: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
|
||||
}, moderator?: MiUser): Promise<MiEmoji> {
|
||||
const emoji = await this.emojisRepository.insert({
|
||||
|
@ -81,7 +120,6 @@ 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,7 +149,6 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
draft: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
|
@ -126,7 +163,6 @@ 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,
|
||||
|
@ -161,6 +197,36 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateRequest(id: MiEmojiRequest['id'], data: {
|
||||
driveFile?: MiDriveFile;
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojiRequestsRepository.findOneBy({ name: data.name });
|
||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||
|
||||
await this.emojiRequestsRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
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,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
|
@ -232,36 +298,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
@bindThis
|
||||
public async setLocalOnlyBulk(ids: MiEmoji['id'][], localOnly: boolean | false) {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
localOnly: localOnly,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
@bindThis
|
||||
public async setisSensitiveBulk(ids: MiEmoji['id'][], isSensitive: boolean | false) {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
isSensitive: isSensitive,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
@bindThis
|
||||
public async setLicenseBulk(ids: MiEmoji['id'][], license: string | null) {
|
||||
await this.emojisRepository.update({
|
||||
|
@ -298,6 +335,13 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteRequest(id: MiEmojiRequest['id']) {
|
||||
const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id });
|
||||
|
||||
await this.emojiRequestsRepository.delete(emoji.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
|
@ -419,11 +463,21 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public checkRequestDuplicate(name: string): Promise<boolean> {
|
||||
return this.emojiRequestsRepository.exist({ where: { name } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiById(id: string): Promise<MiEmoji | null> {
|
||||
return this.emojisRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiRequestById(id: string): Promise<MiEmojiRequest | null> {
|
||||
return this.emojiRequestsRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.cache.dispose();
|
||||
|
|
|
@ -72,6 +72,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
inviteLimitCycle: 60 * 24 * 7,
|
||||
inviteExpirationTime: 0,
|
||||
canManageCustomEmojis: false,
|
||||
canRequestCustomEmojis: false,
|
||||
canManageAvatarDecorations: false,
|
||||
canRequestCustomEmojis: false,
|
||||
canSearchNotes: false,
|
||||
|
@ -336,6 +337,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
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)),
|
||||
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
|
||||
canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
|
|
|
@ -133,7 +133,10 @@ export class DriveFileEntityService {
|
|||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getFromUrl(url: string): Promise<MiDriveFile | null> {
|
||||
return this.driveFilesRepository.findOneBy({ url: url });
|
||||
}
|
||||
@bindThis
|
||||
public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise<number> {
|
||||
const id = typeof user === 'object' ? user.id : user;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojiRequestsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
|
||||
@Injectable()
|
||||
export class EmojiRequestsEntityService {
|
||||
constructor(
|
||||
@Inject(DI.emojiRequestsRepository)
|
||||
private emojiRequestsRepository: EmojiRequestsRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packSimple(
|
||||
src: MiEmojiRequest['id'] | MiEmojiRequest,
|
||||
): Promise<Packed<'EmojiRequestSimple'>> {
|
||||
const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
aliases: emoji.aliases,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl,
|
||||
isSensitive: emoji.isSensitive ? true : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packSimpleMany(
|
||||
emojis: any[],
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.packSimple(x)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiEmojiRequest['id'] | MiEmojiRequest,
|
||||
): Promise<Packed<'EmojiRequestDetailed'>> {
|
||||
const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: emoji.id,
|
||||
aliases: emoji.aliases,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
url: emoji.publicUrl,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
fileId: emoji.fileId,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packDetailedMany(
|
||||
emojis: any[],
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.packDetailed(x)));
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ export const DI = {
|
|||
followRequestsRepository: Symbol('followRequestsRepository'),
|
||||
instancesRepository: Symbol('instancesRepository'),
|
||||
emojisRepository: Symbol('emojisRepository'),
|
||||
emojiRequestsRepository: Symbol('emojiRequestsRepository'),
|
||||
driveFilesRepository: Symbol('driveFilesRepository'),
|
||||
driveFoldersRepository: Symbol('driveFoldersRepository'),
|
||||
metasRepository: Symbol('metasRepository'),
|
||||
|
|
|
@ -33,7 +33,7 @@ import { packedClipSchema } from '@/models/json-schema/clip.js';
|
|||
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
|
||||
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
||||
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
|
||||
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
||||
import { packedEmojiDetailedSchema, packedEmojiRequestSimpleSchema, packedEmojiSimpleSchema, packedEmojiRequestDetailedSchema } from '@/models/json-schema/emoji.js';
|
||||
import { packedFlashSchema } from '@/models/json-schema/flash.js';
|
||||
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||
|
@ -73,7 +73,9 @@ export const refs = {
|
|||
FederationInstance: packedFederationInstanceSchema,
|
||||
GalleryPost: packedGalleryPostSchema,
|
||||
EmojiSimple: packedEmojiSimpleSchema,
|
||||
EmojiRequestSimple: packedEmojiRequestSimpleSchema,
|
||||
EmojiDetailed: packedEmojiDetailedSchema,
|
||||
EmojiRequestDetailed: packedEmojiRequestDetailedSchema,
|
||||
Flash: packedFlashSchema,
|
||||
Signin: packedSigninSchema,
|
||||
RoleLite: packedRoleLiteSchema,
|
||||
|
|
71
packages/backend/src/models/EmojiRequest.ts
Normal file
71
packages/backend/src/models/EmojiRequest.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
|
||||
@Entity('emoji_request')
|
||||
@Index(['name'], { unique: true })
|
||||
export class MiEmojiRequest {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public updatedAt: Date | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public category: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
})
|
||||
public originalUrl: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
default: '',
|
||||
})
|
||||
public publicUrl: string;
|
||||
|
||||
// publicUrlの方のtypeが入る
|
||||
@Column('varchar', {
|
||||
length: 64, nullable: true,
|
||||
})
|
||||
public type: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
array: true, length: 128, default: '{}',
|
||||
})
|
||||
public aliases: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: true,
|
||||
})
|
||||
public license: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: false,
|
||||
})
|
||||
public fileId: string;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public localOnly: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isSensitive: boolean;
|
||||
}
|
|
@ -25,7 +25,7 @@ import {
|
|||
MiDriveFile,
|
||||
MiDriveFolder,
|
||||
MiEmoji,
|
||||
MiFlash,
|
||||
MiEmojiRequest,MiFlash,
|
||||
MiFlashLike,
|
||||
MiFollowRequest,
|
||||
MiFollowing,
|
||||
|
@ -244,6 +244,12 @@ const $emojisRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $emojiRequestsRepository: Provider = {
|
||||
provide: DI.emojiRequestsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiEmojiRequest),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $driveFilesRepository: Provider = {
|
||||
provide: DI.driveFilesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
|
||||
|
@ -504,6 +510,7 @@ const $userMemosRepository: Provider = {
|
|||
$followRequestsRepository,
|
||||
$instancesRepository,
|
||||
$emojisRepository,
|
||||
$emojiRequestsRepository,
|
||||
$driveFilesRepository,
|
||||
$driveFoldersRepository,
|
||||
$metasRepository,
|
||||
|
@ -572,6 +579,7 @@ const $userMemosRepository: Provider = {
|
|||
$followRequestsRepository,
|
||||
$instancesRepository,
|
||||
$emojisRepository,
|
||||
$emojiRequestsRepository,
|
||||
$driveFilesRepository,
|
||||
$driveFoldersRepository,
|
||||
$metasRepository,
|
||||
|
|
|
@ -21,6 +21,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js';
|
|||
import { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||
import { MiEmoji } from '@/models/Emoji.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
import { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
|
@ -90,6 +91,7 @@ export {
|
|||
MiDriveFile,
|
||||
MiDriveFolder,
|
||||
MiEmoji,
|
||||
MiEmojiRequest,
|
||||
MiFollowing,
|
||||
MiFollowRequest,
|
||||
MiGalleryLike,
|
||||
|
@ -158,6 +160,7 @@ export type ClipFavoritesRepository = Repository<MiClipFavorite>;
|
|||
export type DriveFilesRepository = Repository<MiDriveFile>;
|
||||
export type DriveFoldersRepository = Repository<MiDriveFolder>;
|
||||
export type EmojisRepository = Repository<MiEmoji>;
|
||||
export type EmojiRequestsRepository = Repository<MiEmojiRequest>;
|
||||
export type FollowingsRepository = Repository<MiFollowing>;
|
||||
export type FollowRequestsRepository = Repository<MiFollowRequest>;
|
||||
export type GalleryLikesRepository = Repository<MiGalleryLike>;
|
||||
|
|
|
@ -46,6 +46,36 @@ export const packedEmojiSimpleSchema = {
|
|||
},
|
||||
},
|
||||
} as const;
|
||||
export const packedEmojiRequestSimpleSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
aliases: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedEmojiDetailedSchema = {
|
||||
type: 'object',
|
||||
|
@ -108,3 +138,51 @@ export const packedEmojiDetailedSchema = {
|
|||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedEmojiRequestDetailedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
aliases: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
license: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
isSensitive: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localOnly: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -29,6 +29,7 @@ import { MiClipFavorite } from '@/models/ClipFavorite.js';
|
|||
import { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||
import { MiEmoji } from '@/models/Emoji.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
import { MiFollowRequest } from '@/models/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/GalleryLike.js';
|
||||
|
@ -164,6 +165,7 @@ export const entities = [
|
|||
MiPoll,
|
||||
MiPollVote,
|
||||
MiEmoji,
|
||||
MiEmojiRequest,
|
||||
MiHashtag,
|
||||
MiSwSubscription,
|
||||
MiAbuseUserReport,
|
||||
|
|
|
@ -34,18 +34,20 @@ 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_addRequest from './endpoints/admin/emoji/add-request.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';
|
||||
import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
|
||||
import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
|
||||
import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
|
||||
import * as ep___admin_emoji_listRequest from './endpoints/admin/emoji/list-request.js';
|
||||
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
|
||||
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
||||
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
||||
import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js';
|
||||
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
||||
|
@ -255,6 +257,7 @@ import * as ep___invite_list from './endpoints/invite/list.js';
|
|||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emojiRequests from './endpoints/emoji-requests.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||
|
@ -402,18 +405,20 @@ 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_addRequest: Provider = { provide: 'ep:admin/emoji/add-request', useClass: ep___admin_emoji_addRequest.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 };
|
||||
const $admin_emoji_importZip: Provider = { provide: 'ep:admin/emoji/import-zip', useClass: ep___admin_emoji_importZip.default };
|
||||
const $admin_emoji_listRemote: Provider = { provide: 'ep:admin/emoji/list-remote', useClass: ep___admin_emoji_listRemote.default };
|
||||
const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass: ep___admin_emoji_list.default };
|
||||
const $admin_emoji_listRequest: Provider = { provide: 'ep:admin/emoji/list-request', useClass: ep___admin_emoji_listRequest.default };
|
||||
const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default };
|
||||
const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default };
|
||||
const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
|
||||
const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
|
||||
const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
|
||||
const $admin_emoji_updateRequest: Provider = { provide: 'ep:admin/emoji/update-request', useClass: ep___admin_emoji_updateRequest.default };
|
||||
const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
|
||||
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
|
||||
const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
|
||||
|
@ -624,6 +629,7 @@ const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invit
|
|||
const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
|
||||
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
||||
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
||||
const $emoji_requests: Provider = { provide: 'ep:emoji-requests', useClass: ep___emojiRequests.default };
|
||||
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
|
||||
const $miauth_genToken: Provider = { provide: 'ep:miauth/gen-token', useClass: ep___miauth_genToken.default };
|
||||
const $mute_create: Provider = { provide: 'ep:mute/create', useClass: ep___mute_create.default };
|
||||
|
@ -774,18 +780,20 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_emoji_setlocalOnlyBulk,
|
||||
$admin_emoji_setisSensitiveBulk,
|
||||
$admin_emoji_add,
|
||||
$admin_emoji_addDraft,
|
||||
$admin_emoji_addRequest,
|
||||
$admin_emoji_copy,
|
||||
$admin_emoji_deleteBulk,
|
||||
$admin_emoji_delete,
|
||||
$admin_emoji_importZip,
|
||||
$admin_emoji_listRemote,
|
||||
$admin_emoji_list,
|
||||
$admin_emoji_listRequest,
|
||||
$admin_emoji_removeAliasesBulk,
|
||||
$admin_emoji_setAliasesBulk,
|
||||
$admin_emoji_setCategoryBulk,
|
||||
$admin_emoji_setLicenseBulk,
|
||||
$admin_emoji_update,
|
||||
$admin_emoji_updateRequest,
|
||||
$admin_federation_deleteAllFiles,
|
||||
$admin_federation_refreshRemoteInstanceMetadata,
|
||||
$admin_federation_removeAllFollowing,
|
||||
|
@ -996,6 +1004,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji_requests,
|
||||
$emoji,
|
||||
$miauth_genToken,
|
||||
$mute_create,
|
||||
|
@ -1138,13 +1147,14 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_drive_showFile,
|
||||
$admin_emoji_addAliasesBulk,
|
||||
$admin_emoji_add,
|
||||
$admin_emoji_addDraft,
|
||||
$admin_emoji_addRequest,
|
||||
$admin_emoji_copy,
|
||||
$admin_emoji_deleteBulk,
|
||||
$admin_emoji_delete,
|
||||
$admin_emoji_importZip,
|
||||
$admin_emoji_listRemote,
|
||||
$admin_emoji_list,
|
||||
$admin_emoji_listRequest,
|
||||
$admin_emoji_removeAliasesBulk,
|
||||
$admin_emoji_setAliasesBulk,
|
||||
$admin_emoji_setCategoryBulk,
|
||||
|
@ -1152,6 +1162,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_emoji_setlocalOnlyBulk,
|
||||
$admin_emoji_setisSensitiveBulk,
|
||||
$admin_emoji_update,
|
||||
$admin_emoji_updateRequest,
|
||||
$admin_federation_deleteAllFiles,
|
||||
$i_userstats,
|
||||
$admin_federation_refreshRemoteInstanceMetadata,
|
||||
|
@ -1362,6 +1373,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji_requests,
|
||||
$emoji,
|
||||
$miauth_genToken,
|
||||
$mute_create,
|
||||
|
|
|
@ -33,18 +33,20 @@ 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_addRequest from './endpoints/admin/emoji/add-request.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';
|
||||
import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
|
||||
import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
|
||||
import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
|
||||
import * as ep___admin_emoji_listRequest from './endpoints/admin/emoji/list-request.js';
|
||||
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
|
||||
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
||||
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
||||
import * as ep___admin_emoji_updateRequest from './endpoints/admin/emoji/update-request.js';
|
||||
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
|
||||
|
@ -255,6 +257,7 @@ import * as ep___invite_list from './endpoints/invite/list.js';
|
|||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emojiRequests from './endpoints/emoji-requests.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
|
||||
import * as ep___mute_create from './endpoints/mute/create.js';
|
||||
|
@ -397,13 +400,14 @@ 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/add-request', ep___admin_emoji_addRequest],
|
||||
['admin/emoji/copy', ep___admin_emoji_copy],
|
||||
['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk],
|
||||
['admin/emoji/delete', ep___admin_emoji_delete],
|
||||
['admin/emoji/import-zip', ep___admin_emoji_importZip],
|
||||
['admin/emoji/list-remote', ep___admin_emoji_listRemote],
|
||||
['admin/emoji/list', ep___admin_emoji_list],
|
||||
['admin/emoji/list-request', ep___admin_emoji_listRequest],
|
||||
['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk],
|
||||
['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk],
|
||||
['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
|
||||
|
@ -411,6 +415,7 @@ const eps = [
|
|||
['admin/emoji/set-issensitive-bulk', ep___admin_emoji_setisSensitiveBulk],
|
||||
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
|
||||
['admin/emoji/update', ep___admin_emoji_update],
|
||||
['admin/emoji/update-request', ep___admin_emoji_updateRequest],
|
||||
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
|
||||
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
|
||||
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
|
||||
|
@ -621,6 +626,7 @@ const eps = [
|
|||
['invite/limit', ep___invite_limit],
|
||||
['meta', ep___meta],
|
||||
['emojis', ep___emojis],
|
||||
['emoji-requests', ep___emojiRequests],
|
||||
['emoji', ep___emoji],
|
||||
['miauth/gen-token', ep___miauth_genToken],
|
||||
['mute/create', ep___mute_create],
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
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',
|
||||
},
|
||||
duplicateName: {
|
||||
message: 'Duplicate name.',
|
||||
code: 'DUPLICATE_NAME',
|
||||
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
|
||||
},
|
||||
},
|
||||
} 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 },
|
||||
isSensitive: { type: 'boolean', nullable: true },
|
||||
localOnly: { type: 'boolean', nullable: true },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['name', 'fileId'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
||||
@Injectable()
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
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 isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
||||
const isRequestDuplicate = await this.customEmojiService.checkRequestDuplicate(ps.name);
|
||||
|
||||
if (isDuplicate || isRequestDuplicate) throw new ApiError(meta.errors.duplicateName);
|
||||
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
||||
const emoji = await this.customEmojiService.request({
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
localOnly: ps.localOnly ?? false,
|
||||
});
|
||||
|
||||
await this.moderationLogService.log(me, 'addCustomEmoji', {
|
||||
emojiId: emoji.id,
|
||||
emoji: emoji,
|
||||
});
|
||||
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -38,7 +38,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.customEmojiService.delete(ps.id, me);
|
||||
const emoji = await this.customEmojiService.getEmojiById(ps.id);
|
||||
const RequestEmoji = await this.customEmojiService.getEmojiRequestById(ps.id);
|
||||
if (emoji != null) {
|
||||
await this.customEmojiService.delete(ps.id, me);
|
||||
}
|
||||
if (RequestEmoji != null) {
|
||||
await this.customEmojiService.deleteRequest(ps.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { EmojiRequestsRepository } from '@/models/_.js';
|
||||
import type { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { EmojiRequestsEntityService } from '@/core/entities/EmojiRequestsEntityService.js';
|
||||
//import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
aliases: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', 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' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.emojiRequestsRepository)
|
||||
private emojiRequestsRepository: EmojiRequestsRepository,
|
||||
|
||||
private emojiRequestsEntityService: EmojiRequestsEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const q = this.queryService.makePaginationQuery(this.emojiRequestsRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId);
|
||||
|
||||
let emojis: MiEmojiRequest[];
|
||||
|
||||
if (ps.query) {
|
||||
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
|
||||
//const emojis = await q.limit(ps.limit).getMany();
|
||||
|
||||
emojis = await q.getMany();
|
||||
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
|
||||
|
||||
if (queryarry) {
|
||||
emojis = emojis.filter(emoji =>
|
||||
queryarry.includes(`:${emoji.name}:`),
|
||||
);
|
||||
} else {
|
||||
emojis = emojis.filter(emoji =>
|
||||
emoji.name.includes(ps.query!) ||
|
||||
emoji.aliases.some(a => a.includes(ps.query!)) ||
|
||||
emoji.category?.includes(ps.query!));
|
||||
}
|
||||
emojis.splice(ps.limit + 1);
|
||||
} else {
|
||||
emojis = await q.limit(ps.limit).getMany();
|
||||
}
|
||||
|
||||
return this.emojiRequestsEntityService.packDetailedMany(emojis);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import type { DriveFilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
|
||||
errors: {
|
||||
noSuchEmoji: {
|
||||
message: 'No such emoji.',
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
|
||||
},
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d',
|
||||
},
|
||||
sameNameEmojiExists: {
|
||||
message: 'Emoji that have same name already exists.',
|
||||
code: 'SAME_NAME_EMOJI_EXISTS',
|
||||
id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
},
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
Request: { type: 'boolean' },
|
||||
},
|
||||
required: ['id', 'name', 'aliases'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let driveFile;
|
||||
const isRequest = !!ps.Request;
|
||||
if (ps.fileId) {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
|
||||
const emoji = await this.customEmojiService.getEmojiRequestById(ps.id);
|
||||
if (emoji != null) {
|
||||
if (ps.name !== emoji.name) {
|
||||
const isDuplicate = await this.customEmojiService.checkRequestDuplicate(ps.name);
|
||||
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
|
||||
}
|
||||
} else {
|
||||
throw new ApiError(meta.errors.noSuchEmoji);
|
||||
}
|
||||
if (!isRequest) {
|
||||
const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl);
|
||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||
await this.customEmojiService.add({
|
||||
driveFile: file,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
host: null,
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
localOnly: ps.localOnly ?? false,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
|
||||
}, me);
|
||||
await this.customEmojiService.deleteRequest(ps.id);
|
||||
} else {
|
||||
await this.customEmojiService.updateRequest(ps.id, {
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
localOnly: ps.localOnly ?? false,
|
||||
}, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import type { DriveFilesRepository , EmojisRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -62,7 +63,7 @@ export const paramDef = {
|
|||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
draft: { type: 'boolean' },
|
||||
Request: { type: 'boolean' },
|
||||
},
|
||||
required: ['id', 'name', 'draft', 'aliases'],
|
||||
} as const;
|
||||
|
@ -73,10 +74,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let driveFile;
|
||||
|
||||
const isRequest = !!ps.Request;
|
||||
if (ps.fileId) {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
@ -91,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchEmoji);
|
||||
}
|
||||
|
||||
await this.customEmojiService.update(ps.id, {
|
||||
if (!isRequest) {await this.customEmojiService.update(ps.id, {
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
|
@ -101,7 +103,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
localOnly: ps.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
draft: ps.draft,
|
||||
}, me);
|
||||
}, me);} else {
|
||||
const file = await this.driveFileEntityService.getFromUrl(emoji.originalUrl);
|
||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||
await this.customEmojiService.request({
|
||||
driveFile: file,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
localOnly: ps.localOnly ?? false,
|
||||
}, me);
|
||||
await this.customEmojiService.delete(ps.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
64
packages/backend/src/server/api/endpoints/emoji-requests.ts
Normal file
64
packages/backend/src/server/api/endpoints/emoji-requests.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { EmojiRequestsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { EmojiRequestsEntityService } from '@/core/entities/EmojiRequestsEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: false,
|
||||
allowGet: true,
|
||||
cacheSec: 3600,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
emojis: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'EmojiRequestSimple',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.emojiRequestsRepository)
|
||||
private emojiRequestsRepository: EmojiRequestsRepository,
|
||||
|
||||
private emojiRequestsEntityService: EmojiRequestsEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async () => {
|
||||
const emojis = await this.emojiRequestsRepository.find({
|
||||
order: {
|
||||
category: 'ASC',
|
||||
name: 'ASC',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
emojis: await this.emojiRequestsEntityService.packSimpleMany(emojis),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
|
@ -57,6 +57,7 @@ export const moderationLogTypes = [
|
|||
'unsuspend',
|
||||
'updateUserNote',
|
||||
'addCustomEmoji',
|
||||
'requestCustomEmoji',
|
||||
'updateCustomEmoji',
|
||||
'deleteCustomEmoji',
|
||||
'assignRole',
|
||||
|
@ -117,6 +118,10 @@ export type ModerationLogPayloads = {
|
|||
emojiId: string;
|
||||
emoji: any;
|
||||
};
|
||||
requestCustomEmoji: {
|
||||
emojiId: string;
|
||||
emoji: any;
|
||||
};
|
||||
updateCustomEmoji: {
|
||||
emojiId: string;
|
||||
before: any;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<MkInput v-model="query" :debounce="true" type="search">
|
||||
<MkInput v-model="query" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
|
@ -20,22 +20,22 @@
|
|||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="100">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div :class="$style.root">
|
||||
<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.url" 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)">
|
||||
<button v-if="emoji.request" class="_panel _button" :class="[{ selected: selectedEmojis.includes(emoji.id) },$style.emoji,$style.emojirequest]" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button v-else class="_panel _button" :class="[{ selected: selectedEmojis.includes(emoji.id) },$style.emoji]" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
|
||||
<img :src="emoji.url" :class="$style.img" :alt="emoji.name"/>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.name" class="_monospace">{{ emoji.name }}</div>
|
||||
<div :class="$style.info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -64,6 +64,14 @@ const pagination = {
|
|||
query: (query.value && query.value !== '') ? query.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (selectedEmojis.value.length > 0) {
|
||||
selectedEmojis.value = [];
|
||||
} else {
|
||||
selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
|
||||
}
|
||||
};
|
||||
const setisSensitiveBulk = async () => {
|
||||
const { canceled, result } = await os.switch1({
|
||||
title: 'isSensitive',
|
||||
|
@ -221,53 +229,48 @@ const delBulk = async () => {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ldhfsamy {
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: var(--margin);
|
||||
}
|
||||
.emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
border: solid 1px var(--panel);
|
||||
width: 100%;
|
||||
|
||||
div > .emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
border: solid 1px var(--panel);
|
||||
width: 100%;
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputBorderHover);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
> .img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
> .name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .info {
|
||||
opacity: 0.5;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
.img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
.body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.emoji-draft {
|
||||
.info {
|
||||
opacity: 0.5;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.emojirequest {
|
||||
--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;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<FormSplit>
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search">
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
<template #label>{{ i18n.ts.search }}</template>
|
||||
</MkInput>
|
||||
|
@ -8,15 +8,15 @@
|
|||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination" :displayLimit="100">
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
<div :class="$style.root">
|
||||
<div v-for="emoji in items" :key="emoji.id" :class="$style.emoji" class="_panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="`/emoji/${emoji.name}@${emoji.host}.webp`" :class="$style.img" :alt="emoji.name"/>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.name" class="_monospace">{{ emoji.name }}</div>
|
||||
<div :class="$style.info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -62,49 +62,45 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
<style lang="scss" module>
|
||||
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
}
|
||||
|
||||
.ldhfsamy {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin) 0;
|
||||
.emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
|
||||
> .emoji {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
.img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
> .img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
> .name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .info {
|
||||
opacity: 0.5;
|
||||
font-size: 90%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info {
|
||||
opacity: 0.5;
|
||||
font-size: 90%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
198
packages/frontend/src/components/MkCustomEmojiEditRequest.vue
Normal file
198
packages/frontend/src/components/MkCustomEmojiEditRequest.vue
Normal file
|
@ -0,0 +1,198 @@
|
|||
<template>
|
||||
<MkPagination ref="emojisRequestPaginationComponent" :pagination="paginationRequest">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<template v-for="emoji in items" :key="emoji.id">
|
||||
<div :class="$style.emoji" class="_panel">
|
||||
<div :class="$style.img">
|
||||
<div :class="$style.imgLight"><img :src="emoji.url" :alt="emoji.name"/></div>
|
||||
<div :class="$style.imgDark"><img :src="emoji.url" :alt="emoji.name"/></div>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<div :class="$style.name">{{ i18n.ts.name }}: {{ emoji.name }}</div>
|
||||
<div :class="$style.category">{{ i18n.ts.category }}:{{ emoji.category }}</div>
|
||||
<div :class="$style.aliases">{{ i18n.ts.tags }}:{{ emoji.aliases.join(' ') }}</div>
|
||||
<div :class="$style.license">{{ i18n.ts.license }}:{{ emoji.license }}</div>
|
||||
</div>
|
||||
<div :class="$style.editbutton">
|
||||
<MkButton primary :class="$style.edit" @click="editRequest(emoji)">
|
||||
{{ i18n.ts.edit }}
|
||||
</MkButton>
|
||||
<MkButton :class="$style.request" @click="unrequested(emoji)">
|
||||
{{ i18n.ts.approval }}
|
||||
</MkButton>
|
||||
<MkButton danger :class="$style.delete" @click="deleteRequest(emoji)">
|
||||
{{ i18n.ts.delete }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, shallowRef } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const emojisRequestPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const query = ref(null);
|
||||
|
||||
const paginationRequest = {
|
||||
endpoint: 'admin/emoji/list-request' as const,
|
||||
limit: 30,
|
||||
params: computed(() => ({
|
||||
query: (query.value && query.value !== '') ? query.value : null,
|
||||
})),
|
||||
};
|
||||
|
||||
function editRequest(emoji) {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
emoji: emoji,
|
||||
isRequest: true,
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisRequestPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
...oldEmoji,
|
||||
...result.updated,
|
||||
}));
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
} else if (result.deleted) {
|
||||
emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
}
|
||||
|
||||
async function unrequested(emoji) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('requestApprovalAreYouSure', { x: emoji.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
await os.api('admin/emoji/update-request', {
|
||||
id: emoji.id,
|
||||
fileId: emoji.fileId,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
aliases: emoji.aliases,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
isRequest: false,
|
||||
});
|
||||
|
||||
emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
}
|
||||
|
||||
async function deleteRequest(emoji) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: emoji.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.api('admin/emoji/delete', {
|
||||
id: emoji.id,
|
||||
}).then(() => {
|
||||
emojisRequestPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
emojisRequestPaginationComponent.value.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.emoji {
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
text-align: left;
|
||||
border: solid 1px var(--panel);
|
||||
margin: 10px;
|
||||
}
|
||||
.img {
|
||||
display: grid;
|
||||
grid-row: 1;
|
||||
grid-column: 1/ span 2;
|
||||
grid-template-columns: 50% 50%;
|
||||
place-content: center;
|
||||
place-items: center;
|
||||
}
|
||||
.imgLight {
|
||||
display: grid;
|
||||
grid-column: 1;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
img {
|
||||
max-height: 64px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.imgDark {
|
||||
display: grid;
|
||||
grid-column: 2;
|
||||
background-color: #000;
|
||||
margin-bottom: 12px;
|
||||
img {
|
||||
max-height: 64px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.info {
|
||||
display: grid;
|
||||
grid-row: 2;
|
||||
grid-template-rows: 30px 30px 30px;
|
||||
}
|
||||
.name {
|
||||
grid-row: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category {
|
||||
grid-row: 2;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aliases {
|
||||
grid-row: 3;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.license {
|
||||
grid-row: 4;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.editbutton {
|
||||
display: grid;
|
||||
grid-template-rows: 42px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.edit {
|
||||
grid-row: 1;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.request {
|
||||
grid-row: 2;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.delete {
|
||||
grid-row: 3;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
}
|
||||
</style>
|
|
@ -7,12 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="400"
|
||||
:withOkButton="false "
|
||||
@close="dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
|
||||
<template v-else-if="isRequest" #header>{{ i18n.ts.requestCustomEmojis }}</template>
|
||||
<template v-else-if="isRequest && !emoji" #header>{{ i18n.ts.requestCustomEmojis }}</template>
|
||||
<template v-else #header>New emoji</template>
|
||||
|
||||
<div>
|
||||
|
@ -64,14 +63,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo warn>{{ i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionPublicRoleWarn }}</MkInfo>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<MkSwitch v-model="isSensitive">isSensitive</MkSwitch>
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="isSensitive">{{ i18n.ts.isSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="isNotifyIsHome">
|
||||
{{ i18n.ts.isNotifyIsHome }}
|
||||
</MkSwitch>
|
||||
<MkSwitch v-if="!isRequest" v-model="draft" >
|
||||
{{ i18n.ts.draft }}
|
||||
</MkSwitch>
|
||||
|
||||
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
@ -87,7 +86,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { DriveFile } from 'misskey-js/built/entities.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
|
@ -99,144 +98,91 @@ import * as os from '@/os.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { customEmojiCategories } from '@/custom-emojis.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { selectFile } from '@/scripts/select-file.js';
|
||||
import { selectFile, selectFiles } from '@/scripts/select-file.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji?: any,
|
||||
isRequest: boolean,
|
||||
emoji?: any,
|
||||
isRequest: boolean,
|
||||
}>();
|
||||
|
||||
const dialog = ref(null);
|
||||
const name = ref<string>(props.emoji ? props.emoji.name : '');
|
||||
const category = ref<string>(props.emoji ? props.emoji.category : '');
|
||||
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : '');
|
||||
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
|
||||
const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
|
||||
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
const rolesThatCanBeUsedThisEmojiAsReaction = ref([]);
|
||||
const file = ref<Misskey.entities.DriveFile>();
|
||||
let chooseFile = ref(null);
|
||||
let draft = ref(props.emoji ? props.emoji.draft : false);
|
||||
let isRequest = ref(props.isRequest);
|
||||
let isNotifyIsHome = ref(false);
|
||||
let url;
|
||||
watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||
let dialog = $ref(null);
|
||||
let name: string = $ref(props.emoji ? props.emoji.name : '');
|
||||
let category: string = $ref(props.emoji ? props.emoji.category : '');
|
||||
let aliases: string = $ref(props.emoji ? props.emoji.aliases.join(' ') : '');
|
||||
let license: string = $ref(props.emoji ? (props.emoji.license ?? '') : '');
|
||||
let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false);
|
||||
let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
|
||||
let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref((props.emoji && props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
|
||||
let file = $ref<Misskey.entities.DriveFile>();
|
||||
let chooseFile: DriveFile|null = $ref(null);
|
||||
let isRequest = $ref(props.isRequest ?? false);
|
||||
watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||
}, { immediate: true });
|
||||
|
||||
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
|
||||
const imgUrl = computed(() => file ? file.url : props.emoji && !isRequest ? `/emoji/${props.emoji.name}.webp` : props.emoji && props.emoji.url ? props.emoji.url : null);
|
||||
const validation = computed(() => {
|
||||
return name.value.match(/^[a-zA-Z0-9_]+$/) && imgUrl.value != null;
|
||||
return name.match(/^[a-zA-Z0-9_]+$/) && imgUrl.value != null;
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
function ok() {
|
||||
if (isRequest) {
|
||||
if (chooseFile.value !== null && name.value.match(/^[a-zA-Z0-9_]+$/)) {
|
||||
add();
|
||||
}
|
||||
} else {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
async function add() {
|
||||
const ret = await os.api('admin/emoji/add-draft', {
|
||||
name: name,
|
||||
category: category.value,
|
||||
aliases: aliases.value.split(' '),
|
||||
license: license.value === '' ? null : license.value,
|
||||
fileId: chooseFile.value.id,
|
||||
isNotifyIsHome: isNotifyIsHome.value,
|
||||
});
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: ret.value.id,
|
||||
name,
|
||||
category,
|
||||
aliases: aliases.value.split(' '),
|
||||
license: license.value === '' ? null : license,
|
||||
draft: true,
|
||||
},
|
||||
});
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
async function changeImage(ev) {
|
||||
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.value.name.replace(/\.(.+)$/, '');
|
||||
file = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.name.replace(/\.(.+)$/, '');
|
||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||
name.value = candidate;
|
||||
name = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
async function addRole() {
|
||||
const roles = await os.api('admin/roles/list');
|
||||
const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id);
|
||||
const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id);
|
||||
|
||||
const { canceled, result: role } = await os.select({
|
||||
items: roles.filter(r => r.isPublic).filter(r => !currentRoleIds.includes(r.id)).map(r => ({ text: r.name, value: r })),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.push(role);
|
||||
}
|
||||
|
||||
async function removeRole(role, ev) {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.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: name.value,
|
||||
category: category.value === '' ? null : category.value,
|
||||
aliases: aliases.value.split(' ').filter(x => x !== ''),
|
||||
license: license.value === '' ? null : license.value,
|
||||
fileId: chooseFile.value?.id,
|
||||
draft: draft.value,
|
||||
});
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: props.emoji.id,
|
||||
name: name.value,
|
||||
category: category.value === '' ? null : category.value,
|
||||
aliases: aliases.value.split(' ').filter(x => x !== ''),
|
||||
license: license.value === '' ? null : license.value,
|
||||
draft: draft.value,
|
||||
},
|
||||
});
|
||||
|
||||
dialog.value.close();
|
||||
}
|
||||
async function done() {
|
||||
const params = {
|
||||
name: name.value,
|
||||
category: category.value === '' ? null : category.value,
|
||||
aliases: aliases.value.split(' ').filter(x => x !== ''),
|
||||
license: license.value === '' ? null : license.value,
|
||||
isSensitive: isSensitive.value,
|
||||
draft: draft.value,
|
||||
localOnly: localOnly.value,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id),
|
||||
isNotifyIsHome: isNotifyIsHome.value,
|
||||
};
|
||||
name,
|
||||
category: category === '' ? null : category,
|
||||
aliases: aliases.replace(' ', ' ').split(' ').filter(x => x !== ''),
|
||||
license: license === '' ? null : license,
|
||||
Request: isRequest,
|
||||
isSensitive,
|
||||
localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: rolesThatCanBeUsedThisEmojiAsReaction.map(x => x.id),
|
||||
};
|
||||
|
||||
if (file.value) {
|
||||
params.fileId = file.value.id;
|
||||
if (file) {
|
||||
params.fileId = file.id;
|
||||
}
|
||||
console.log(props.emoji);
|
||||
|
||||
if (props.emoji) {
|
||||
await os.apiWithDialog('admin/emoji/update', {
|
||||
id: props.emoji.id,
|
||||
...params,
|
||||
});
|
||||
if (isRequest) {
|
||||
await os.apiWithDialog('admin/emoji/update-request', {
|
||||
id: props.emoji.id,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
await os.apiWithDialog('admin/emoji/update', {
|
||||
id: props.emoji.id,
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
|
@ -245,31 +191,24 @@ async function done() {
|
|||
},
|
||||
});
|
||||
|
||||
dialog.value.close();
|
||||
dialog.close();
|
||||
} else {
|
||||
const created = isRequest
|
||||
? await os.apiWithDialog('admin/emoji/add-draft', params)
|
||||
? await os.apiWithDialog('admin/emoji/add-request', params)
|
||||
: await os.apiWithDialog('admin/emoji/add', params);
|
||||
|
||||
emit('done', {
|
||||
created: created,
|
||||
});
|
||||
|
||||
dialog.value.close();
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
function chooseFileFrom(ev) {
|
||||
selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
|
||||
chooseFile.value = files_[0];
|
||||
url = chooseFile.value.url;
|
||||
});
|
||||
}
|
||||
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: name.value }),
|
||||
text: i18n.t('removeAreYouSure', { x: name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
|
@ -279,60 +218,60 @@ async function del() {
|
|||
emit('done', {
|
||||
deleted: true,
|
||||
});
|
||||
dialog.value.close();
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.imgs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.imgContainer {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.img {
|
||||
display: block;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.roleItem {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roleUnassign {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 12px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
}
|
||||
|
||||
.footerButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -212,7 +212,6 @@ async function init(): Promise<void> {
|
|||
const item = res[i];
|
||||
if (i === 3) item._shouldInsertAd_ = true;
|
||||
}
|
||||
|
||||
if (res.length === 0 || props.pagination.noPaging) {
|
||||
concatItems(res);
|
||||
more.value = false;
|
||||
|
@ -221,7 +220,6 @@ async function init(): Promise<void> {
|
|||
concatItems(res);
|
||||
more.value = true;
|
||||
}
|
||||
|
||||
offset.value = res.length;
|
||||
error.value = false;
|
||||
fetching.value = false;
|
||||
|
|
|
@ -5,138 +5,112 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
|
||||
</template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer v-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<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 style="margin-top: 8px;" @click="edit"
|
||||
>
|
||||
{{ i18n.ts.requestCustomEmojis }}
|
||||
</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.canRequestCustomEmojis)" primary @click="edit">{{ i18n.ts.requestCustomEmojis }}</MkButton>
|
||||
|
||||
<div class="query" style="margin-top: 10px;">
|
||||
<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<!-- たくさんあると邪魔
|
||||
<div class="tags">
|
||||
<span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
|
||||
</div>
|
||||
-->
|
||||
<!-- たくさんあると邪魔
|
||||
<div class="tags">
|
||||
<span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="searchEmojis">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji" :draft="emoji.draft"/>
|
||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" :emoji="emoji" :request="emoji.request"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
|
||||
<MkFoldableSection v-for="category in filteredCategories" v-once :key="category">
|
||||
<MkFoldableSection v-for="category in customEmojiCategories" v-once :key="category">
|
||||
<template #header>{{ category || i18n.ts.other }}</template>
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji
|
||||
v-for="emoji in customEmojis.filter(e => e.category === category && !e.draft)" :key="emoji.name"
|
||||
:emoji="emoji" :draft="emoji.draft"
|
||||
/>
|
||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" :emoji="emoji"/>
|
||||
</div>
|
||||
</MkFoldableSection>
|
||||
</MkSpacer>
|
||||
|
||||
<MkSpacer v-if="tab === 'draft'" :contentMax="1000" :marginMin="20">
|
||||
<MkSpacer v-if="tab === 'request'" :contentMax="1000" :marginMin="20">
|
||||
<div :class="$style.emojis">
|
||||
<XEmoji v-for="emoji in draftEmojis" :key="emoji.name" :emoji="emoji" :draft="emoji.draft"/>
|
||||
<XEmoji v-for="emoji in requestEmojis.emojis" :key="emoji.name" :emoji="emoji" :request="true"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { watch, defineAsyncComponent, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import {customEmojis, customEmojiCategories, getCustomEmojiTags} from '@/custom-emojis.js';
|
||||
import { customEmojis, customEmojiCategories } from '@/custom-emojis.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
let tab = ref('emojis');
|
||||
const headerActions = computed(() => []);
|
||||
let tab = $ref('emojis');
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
const customEmojiTags = getCustomEmojiTags();
|
||||
const q = ref('');
|
||||
const searchEmojis = ref<Misskey.entities.EmojiSimple[]>(null);
|
||||
const selectedTags = ref(new Set());
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'emojis',
|
||||
title: i18n.ts.list,
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'emojis',
|
||||
title: i18n.ts.list,
|
||||
}, {
|
||||
key: 'draft',
|
||||
title: i18n.ts.draftEmojis,
|
||||
key: 'request',
|
||||
title: i18n.ts.requestingEmojis,
|
||||
}]);
|
||||
const filteredCategories = computed(() => {
|
||||
return customEmojiCategories.value.filter((category: any) => {
|
||||
return customEmojis.value.some((em: any) => em.category === category && !em.draft);
|
||||
});
|
||||
});
|
||||
|
||||
definePageMetadata(ref({}));
|
||||
const draftEmojis = customEmojis.value.filter(emoji => emoji.draft);
|
||||
|
||||
let q = $ref('');
|
||||
let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null);
|
||||
let selectedTags = $ref(new Set());
|
||||
const requestEmojis = await os.apiGet('emoji-requests');
|
||||
|
||||
function search() {
|
||||
if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) {
|
||||
searchEmojis.value = null;
|
||||
if ((q === '' || q == null) && selectedTags.size === 0) {
|
||||
searchEmojis = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedTags.value.size === 0) {
|
||||
const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
|
||||
if (selectedTags.size === 0) {
|
||||
const queryarry = q.match(/\:([a-z0-9_]*)\:/g);
|
||||
|
||||
if (queryarry) {
|
||||
searchEmojis.value = customEmojis.value.filter(emoji =>
|
||||
searchEmojis = customEmojis.value.filter(emoji =>
|
||||
queryarry.includes(`:${emoji.name}:`),
|
||||
);
|
||||
} else {
|
||||
searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
|
||||
searchEmojis = customEmojis.value.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q));
|
||||
}
|
||||
} else {
|
||||
searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t)));
|
||||
searchEmojis = customEmojis.value.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t)));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTag(tag) {
|
||||
if (selectedTags.value.has(tag)) {
|
||||
selectedTags.value.delete(tag);
|
||||
} else {
|
||||
selectedTags.value.add(tag);
|
||||
}
|
||||
}
|
||||
const edit = () => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
isRequest: true,
|
||||
}, {
|
||||
done: result => {
|
||||
window.location.reload();
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
|
||||
watch(q, () => {
|
||||
watch($$(q), () => {
|
||||
search();
|
||||
});
|
||||
const edit = () => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
isRequest: true,
|
||||
}, {
|
||||
done: result => {
|
||||
window.location.reload();
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
watch(selectedTags, () => {
|
||||
|
||||
watch($$(selectedTags), () => {
|
||||
search();
|
||||
}, { deep: true });
|
||||
|
||||
|
|
|
@ -89,9 +89,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSection>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
|
||||
<XEmojis/>
|
||||
</MkSpacer>
|
||||
<XEmojis v-else-if="tab === 'emojis'"/>
|
||||
<MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
|
||||
<XFederation/>
|
||||
</MkSpacer>
|
||||
|
|
|
@ -319,46 +319,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</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 :textConverter="(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.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 :textConverter="(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.canManageAvatarDecorations, 'canManageAvatarDecorations'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageAvatarDecorations }}</template>
|
||||
<template #suffix>
|
||||
|
|
|
@ -8,17 +8,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkCustomEmojiEditLocal/>
|
||||
</div>
|
||||
<div v-if="tab === 'request'" class="request">
|
||||
<MkCustomEmojiEditDraft/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<MkCustomEmojiEditRemote/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkCustomEmojiEditLocal/>
|
||||
</div>
|
||||
<div v-if="tab === 'request'" class="request">
|
||||
<MkCustomEmojiEditRequest/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<MkCustomEmojiEditRemote/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
import MkCustomEmojiEditDraft from '@/components/MkCustomEmojiEditDraft.vue';
|
||||
import MkCustomEmojiEditRequest from '@/components/MkCustomEmojiEditRequest.vue';
|
||||
import MkCustomEmojiEditLocal from '@/components/MkCustomEmojiEditLocal.vue';
|
||||
import MkCustomEmojiEditRemote from '@/components/MkCustomEmojiEditRemote.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
|
@ -34,15 +34,17 @@ import * as os from '@/os';
|
|||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const tab = ref('draft');
|
||||
const tab = ref('request');
|
||||
|
||||
const add = async (ev: MouseEvent) => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkEmojiEditDialog.vue')), {
|
||||
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
|
||||
}, {
|
||||
done: result => {
|
||||
if (result.created) {
|
||||
emojisPaginationComponent.value.prepend(result.created);
|
||||
}
|
||||
//TODO: emitにして追加を反映
|
||||
// if (result.created) {
|
||||
// emojisPaginationComponent.value.prepend(result.created);
|
||||
// emojisPaginationComponent.value.reload();
|
||||
// }
|
||||
},
|
||||
}, 'closed');
|
||||
};
|
||||
|
@ -89,7 +91,7 @@ const menu = (ev: MouseEvent) => {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
const headerActions = $computed(() => [{
|
||||
asFullButton: true,
|
||||
icon: 'ti ti-plus',
|
||||
text: i18n.ts.addEmoji,
|
||||
|
@ -99,9 +101,9 @@ const headerActions = computed(() => [{
|
|||
handler: menu,
|
||||
}]);
|
||||
|
||||
const headerTabs = computed(() => [{
|
||||
key: 'draft',
|
||||
title: i18n.ts.draftEmojis,
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'request',
|
||||
title: i18n.ts.requestingEmojis,
|
||||
}, {
|
||||
key: 'local',
|
||||
title: i18n.ts.local,
|
||||
|
|
|
@ -4,36 +4,35 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<button v-if="emoji.draft" class="zuvgdzyu _button emoji-draft" @click="menu">
|
||||
<img style="max-height: 64px;object-fit: contain;" :src="emoji.url" class="img" loading="lazy"/>
|
||||
<button v-if="request" class="_button emoji-request" :class="$style.root" @click="menu">
|
||||
<img :src="emoji.url" :class="$style.img" loading="lazy"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name + ' (draft)' }}</div>
|
||||
<div class="name _monospace">{{ emoji.name + ' (request)' }}</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"/>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.name">{{ emoji.name }}</div>
|
||||
<div :class="$style.info">{{ emoji.aliases.join(' ') }}</div>
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.aliases.join(' ') }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
emoji: {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
category: string;
|
||||
url: string;
|
||||
draft: boolean;
|
||||
};
|
||||
emoji: {
|
||||
name: string;
|
||||
aliases: string[];
|
||||
category: string;
|
||||
url: string;
|
||||
};
|
||||
request?: boolean;
|
||||
}>();
|
||||
|
||||
function menu(ev) {
|
||||
|
@ -51,10 +50,10 @@ function menu(ev) {
|
|||
text: i18n.ts.info,
|
||||
icon: 'ti ti-info-circle',
|
||||
action: () => {
|
||||
os.apiGet('emoji', { name: props.emoji.name }).then(res => {
|
||||
os.apiGet('emoji-requests', { name: props.emoji.name }).then(res => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`,
|
||||
text: `License: ${res.license}`,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -64,47 +63,45 @@ function menu(ev) {
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
background: var(--panel);
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 0 0 0 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info {
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
text-overflow: ellipsis;
|
||||
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;
|
||||
max-width: 64px;
|
||||
width: 100%;
|
||||
.emoji-request {
|
||||
--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>
|
||||
|
|
Loading…
Reference in a new issue