mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-27 00:13:09 +02:00
Merge branch 'develop'
This commit is contained in:
commit
34f5d81d1f
32 changed files with 262 additions and 147 deletions
|
@ -9,6 +9,13 @@
|
||||||
You should also include the user name that made the change.
|
You should also include the user name that made the change.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 13.2.3 (2023/01/26)
|
||||||
|
### Improvements
|
||||||
|
- カスタム絵文字の更新をリアルタイムで反映するように
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- turnstile-failed: missing-input-secret
|
||||||
|
|
||||||
## 13.2.2 (2023/01/25)
|
## 13.2.2 (2023/01/25)
|
||||||
### Improvements
|
### Improvements
|
||||||
- サーバーのパフォーマンスを改善
|
- サーバーのパフォーマンスを改善
|
||||||
|
|
|
@ -956,9 +956,11 @@ _achievements:
|
||||||
_login3:
|
_login3:
|
||||||
title: "Новачок I"
|
title: "Новачок I"
|
||||||
description: "3 дні користування загально"
|
description: "3 дні користування загально"
|
||||||
|
flavor: "Відсьогодні називайте мене \"Місскіст\""
|
||||||
_login7:
|
_login7:
|
||||||
title: "Новачок II"
|
title: "Новачок II"
|
||||||
description: "7 днів користування загально"
|
description: "7 днів користування загально"
|
||||||
|
flavor: "Ви звикли до цього?"
|
||||||
_login15:
|
_login15:
|
||||||
title: "Новачок III"
|
title: "Новачок III"
|
||||||
description: "15 днів користування загально"
|
description: "15 днів користування загально"
|
||||||
|
@ -971,6 +973,7 @@ _achievements:
|
||||||
_login100:
|
_login100:
|
||||||
title: "Міскієць III"
|
title: "Міскієць III"
|
||||||
description: "100 днів користування загально"
|
description: "100 днів користування загально"
|
||||||
|
flavor: "Цей юзер лютий місскіст"
|
||||||
_login200:
|
_login200:
|
||||||
title: "Завсідник I"
|
title: "Завсідник I"
|
||||||
description: "200 днів користування загально"
|
description: "200 днів користування загально"
|
||||||
|
@ -983,6 +986,7 @@ _achievements:
|
||||||
_login500:
|
_login500:
|
||||||
title: "Ветеран I"
|
title: "Ветеран I"
|
||||||
description: "500 днів користування загально"
|
description: "500 днів користування загально"
|
||||||
|
flavor: "Meine Kameraden, ich liebe sie, die Notizen."
|
||||||
_login600:
|
_login600:
|
||||||
title: "Ветеран II"
|
title: "Ветеран II"
|
||||||
description: "600 днів користування загально"
|
description: "600 днів користування загально"
|
||||||
|
@ -990,13 +994,25 @@ _achievements:
|
||||||
title: "Ветеран III"
|
title: "Ветеран III"
|
||||||
description: "700 днів користування загально"
|
description: "700 днів користування загально"
|
||||||
_login800:
|
_login800:
|
||||||
|
title: "Майстер нотаток I"
|
||||||
description: "800 днів користування загально"
|
description: "800 днів користування загально"
|
||||||
_login900:
|
_login900:
|
||||||
|
title: "Майстер нотаток II"
|
||||||
description: "900 днів користування загально"
|
description: "900 днів користування загально"
|
||||||
_login1000:
|
_login1000:
|
||||||
|
title: "Майстер нотаток III"
|
||||||
description: "1000 днів користування загально"
|
description: "1000 днів користування загально"
|
||||||
flavor: "Дякуємо, що користуєтеся Misskey!"
|
flavor: "Дякуємо, що користуєтеся Misskey!"
|
||||||
|
_myNoteFavorited1:
|
||||||
|
title: "У пошуках зірок"
|
||||||
|
_markedAsCat:
|
||||||
|
flavor: "Я дам тобі ім'я пізніше"
|
||||||
|
_following1:
|
||||||
|
title: "Перша підписка"
|
||||||
|
_following10:
|
||||||
|
title: "Продовжуй, продовжуй"
|
||||||
_following50:
|
_following50:
|
||||||
|
title: "Багато друзів"
|
||||||
description: "Кількість підписок сягнула 50"
|
description: "Кількість підписок сягнула 50"
|
||||||
_following100:
|
_following100:
|
||||||
title: "100 друзів"
|
title: "100 друзів"
|
||||||
|
@ -1013,6 +1029,7 @@ _achievements:
|
||||||
_followers50:
|
_followers50:
|
||||||
description: "Кількість підписників досягла 50"
|
description: "Кількість підписників досягла 50"
|
||||||
_followers100:
|
_followers100:
|
||||||
|
title: "Популярна особа"
|
||||||
description: "Кількість підписників досягла 100"
|
description: "Кількість підписників досягла 100"
|
||||||
_followers300:
|
_followers300:
|
||||||
description: "Кількість підписників досягла 300"
|
description: "Кількість підписників досягла 300"
|
||||||
|
@ -1021,11 +1038,17 @@ _achievements:
|
||||||
_followers1000:
|
_followers1000:
|
||||||
title: "Інфлюенсер"
|
title: "Інфлюенсер"
|
||||||
description: "Кількість підписників досягла 1000"
|
description: "Кількість підписників досягла 1000"
|
||||||
|
_passedSinceAccountCreated1:
|
||||||
|
title: "Перша річниця"
|
||||||
|
_passedSinceAccountCreated2:
|
||||||
|
title: "Друга річниця"
|
||||||
_passedSinceAccountCreated3:
|
_passedSinceAccountCreated3:
|
||||||
|
title: "Третя річниця"
|
||||||
description: "Минуло 3 роки з моменту створення акаунта"
|
description: "Минуло 3 роки з моменту створення акаунта"
|
||||||
_loggedInOnBirthday:
|
_loggedInOnBirthday:
|
||||||
title: "З Днем народження!"
|
title: "З Днем народження!"
|
||||||
_brainDiver:
|
_brainDiver:
|
||||||
|
title: "Brain Diver"
|
||||||
flavor: "Misskey-Misskey La-Tu-Ma"
|
flavor: "Misskey-Misskey La-Tu-Ma"
|
||||||
_role:
|
_role:
|
||||||
priority: "Пріоритет"
|
priority: "Пріоритет"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.2.2",
|
"version": "13.2.3",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -23,9 +23,9 @@ export class CaptchaService {
|
||||||
|
|
||||||
const res = await this.httpRequestService.send(url, {
|
const res = await this.httpRequestService.send(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(params),
|
body: params.toString(),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
}, { throwErrorWhenResponseNotOk: false });
|
}, { throwErrorWhenResponseNotOk: false });
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In, IsNull } from 'typeorm';
|
import { DataSource, In, IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
|
@ -17,6 +19,8 @@ export class CustomEmojiService {
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +46,10 @@ export class CustomEmojiService {
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
|
emoji: await this.emojiEntityService.pack(emoji.id),
|
||||||
|
});
|
||||||
|
|
||||||
return emoji;
|
return emoji;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,10 @@ export class EmojiEntityService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Emoji['id'] | Emoji,
|
src: Emoji['id'] | Emoji,
|
||||||
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
|
opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = { omitHost: true, omitId: true, withUrl: true },
|
||||||
): Promise<Packed<'Emoji'>> {
|
): Promise<Packed<'Emoji'>> {
|
||||||
|
opts = { omitHost: true, omitId: true, withUrl: true, ...opts }
|
||||||
|
|
||||||
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -496,10 +496,10 @@ export class UserEntityService implements OnModuleInit {
|
||||||
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
showTimelineReplies: user.showTimelineReplies ?? falsy,
|
||||||
achievements: profile!.achievements,
|
achievements: profile!.achievements,
|
||||||
loggedInDays: profile!.loggedInDates.length,
|
loggedInDays: profile!.loggedInDates.length,
|
||||||
|
policies: this.roleService.getUserPolicies(user.id),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(opts.includeSecrets ? {
|
...(opts.includeSecrets ? {
|
||||||
policies: this.roleService.getUserPolicies(user.id),
|
|
||||||
email: profile!.email,
|
email: profile!.email,
|
||||||
emailVerified: profile!.emailVerified,
|
emailVerified: profile!.emailVerified,
|
||||||
securityKeysList: profile!.twoFactorEnabled
|
securityKeysList: profile!.twoFactorEnabled
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
const emojis = await this.emojisRepository.findBy({
|
||||||
|
@ -49,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,10 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import rndstr from 'rndstr';
|
import rndstr from 'rndstr';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { DriveFilesRepository, EmojisRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -39,43 +37,26 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
|
||||||
private db: DataSource,
|
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
private customEmojiService: CustomEmojiService,
|
||||||
private emojisRepository: EmojisRepository,
|
|
||||||
|
|
||||||
private emojiEntityService: EmojiEntityService,
|
|
||||||
private idService: IdService,
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
|
||||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
|
|
||||||
const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.insert({
|
const emoji = await this.customEmojiService.add({
|
||||||
id: this.idService.genId(),
|
driveFile,
|
||||||
updatedAt: new Date(),
|
name,
|
||||||
name: name,
|
|
||||||
category: null,
|
category: null,
|
||||||
host: null,
|
|
||||||
aliases: [],
|
aliases: [],
|
||||||
originalUrl: file.url,
|
host: null,
|
||||||
publicUrl: file.webpublicUrl ?? file.url,
|
|
||||||
type: file.webpublicType ?? file.type,
|
|
||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
|
||||||
emoji: await this.emojiEntityService.pack(emoji.id),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -35,6 +37,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
const emojis = await this.emojisRepository.findBy({
|
||||||
|
@ -43,13 +47,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
for (const emoji of emojis) {
|
||||||
await this.emojisRepository.delete(emoji.id);
|
await this.emojisRepository.delete(emoji.id);
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: await this.emojiEntityService.packMany(emojis),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -42,6 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
||||||
|
@ -52,6 +56,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: [ await this.emojiEntityService.pack(emoji) ],
|
||||||
|
});
|
||||||
|
|
||||||
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
|
||||||
emoji: emoji,
|
emoji: emoji,
|
||||||
});
|
});
|
||||||
|
|
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
.take(ps.limit)
|
.take(ps.limit)
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
return this.emojiEntityService.packMany(emojis);
|
return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
emojis = await q.take(ps.limit).getMany();
|
emojis = await q.take(ps.limit).getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.emojiEntityService.packMany(emojis);
|
return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emojis = await this.emojisRepository.findBy({
|
const emojis = await this.emojisRepository.findBy({
|
||||||
|
@ -49,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
|
@ -45,6 +50,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -37,6 +39,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
await this.emojisRepository.update({
|
await this.emojisRepository.update({
|
||||||
|
@ -47,6 +52,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packMany(ps.ids),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { EmojisRepository } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
@ -48,6 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
@Inject(DI.emojisRepository)
|
@Inject(DI.emojisRepository)
|
||||||
private emojisRepository: EmojisRepository,
|
private emojisRepository: EmojisRepository,
|
||||||
|
|
||||||
|
private emojiEntityService: EmojiEntityService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
|
||||||
|
@ -62,6 +67,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.db.queryResultCache!.remove(['meta_emojis']);
|
await this.db.queryResultCache!.remove(['meta_emojis']);
|
||||||
|
|
||||||
|
const updated = await this.emojiEntityService.pack(emoji.id);
|
||||||
|
|
||||||
|
if (emoji.name === ps.name) {
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: [ updated ],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: [ await this.emojiEntityService.pack(emoji) ],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
|
emoji: updated,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 3600,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|
|
@ -49,6 +49,16 @@ export interface BroadcastTypes {
|
||||||
emojiAdded: {
|
emojiAdded: {
|
||||||
emoji: Packed<'Emoji'>;
|
emoji: Packed<'Emoji'>;
|
||||||
};
|
};
|
||||||
|
emojiUpdated: {
|
||||||
|
emojis: Packed<'Emoji'>[];
|
||||||
|
};
|
||||||
|
emojiDeleted: {
|
||||||
|
emojis: {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
[other: string]: any;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserStreamTypes {
|
export interface UserStreamTypes {
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>An error has occurred!</h1>
|
<h1>An error has occurred!</h1>
|
||||||
<button class="button-big" onclick="location.reload(true);">
|
<button class="button-big" onclick="location.reload();">
|
||||||
<span class="button-label-big">Refresh</span>
|
<span class="button-label-big">Refresh</span>
|
||||||
</button>
|
</button>
|
||||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, shallowRef, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import contains from '@/scripts/contains';
|
import contains from '@/scripts/contains';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
||||||
|
@ -61,59 +61,62 @@ type EmojiDef = {
|
||||||
|
|
||||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||||
|
|
||||||
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
const emojiDb = computed(() => {
|
||||||
|
//#region Unicode Emoji
|
||||||
|
const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||||
|
|
||||||
const emjdb: EmojiDef[] = lib.map(x => ({
|
const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
|
||||||
emoji: x.char,
|
emoji: x.char,
|
||||||
name: x.name,
|
|
||||||
url: char2path(x.char),
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const x of lib) {
|
|
||||||
if (x.keywords) {
|
|
||||||
for (const k of x.keywords) {
|
|
||||||
emjdb.push({
|
|
||||||
emoji: x.char,
|
|
||||||
name: k,
|
|
||||||
aliasOf: x.name,
|
|
||||||
url: char2path(x.char),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emjdb.sort((a, b) => a.name.length - b.name.length);
|
|
||||||
|
|
||||||
//#region Construct Emoji DB
|
|
||||||
const emojiDefinitions: EmojiDef[] = [];
|
|
||||||
|
|
||||||
for (const x of customEmojis) {
|
|
||||||
emojiDefinitions.push({
|
|
||||||
name: x.name,
|
name: x.name,
|
||||||
emoji: `:${x.name}:`,
|
url: char2path(x.char),
|
||||||
isCustomEmoji: true,
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
if (x.aliases) {
|
for (const x of lib) {
|
||||||
for (const alias of x.aliases) {
|
if (x.keywords) {
|
||||||
emojiDefinitions.push({
|
for (const k of x.keywords) {
|
||||||
name: alias,
|
unicodeEmojiDB.push({
|
||||||
aliasOf: x.name,
|
emoji: x.char,
|
||||||
emoji: `:${x.name}:`,
|
name: k,
|
||||||
isCustomEmoji: true,
|
aliasOf: x.name,
|
||||||
});
|
url: char2path(x.char),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
|
unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
|
//#region Custom Emoji
|
||||||
//#endregion
|
const customEmojiDB: EmojiDef[] = [];
|
||||||
|
|
||||||
|
for (const x of customEmojis.value) {
|
||||||
|
customEmojiDB.push({
|
||||||
|
name: x.name,
|
||||||
|
emoji: `:${x.name}:`,
|
||||||
|
isCustomEmoji: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (x.aliases) {
|
||||||
|
for (const alias of x.aliases) {
|
||||||
|
customEmojiDB.push({
|
||||||
|
name: alias,
|
||||||
|
aliasOf: x.name,
|
||||||
|
emoji: `:${x.name}:`,
|
||||||
|
isCustomEmoji: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customEmojiDB.sort((a, b) => a.name.length - b.name.length);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
return markRaw([ ...customEmojiDB, ...unicodeEmojiDB ]);
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
emojiDb,
|
emojiDb,
|
||||||
emojiDefinitions,
|
|
||||||
emojilist,
|
emojilist,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -230,27 +233,27 @@ function exec() {
|
||||||
} else if (props.type === 'emoji') {
|
} else if (props.type === 'emoji') {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === '') {
|
||||||
// 最近使った絵文字をサジェスト
|
// 最近使った絵文字をサジェスト
|
||||||
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched: EmojiDef[] = [];
|
const matched: EmojiDef[] = [];
|
||||||
const max = 30;
|
const max = 30;
|
||||||
|
|
||||||
emojiDb.some(x => {
|
emojiDb.value.some(x => {
|
||||||
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matched.length < max) {
|
if (matched.length < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.value.some(x => {
|
||||||
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matched.length < max) {
|
if (matched.length < max) {
|
||||||
emojiDb.some(x => {
|
emojiDb.value.some(x => {
|
||||||
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
||||||
return matched.length === max;
|
return matched.length === max;
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, computed, Ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emojis: string[];
|
emojis: string[] | Ref<string[]>;
|
||||||
initialShown?: boolean;
|
initialShown?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -29,5 +29,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string, event: MouseEvent): void;
|
(ev: 'chosen', v: string, event: MouseEvent): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emojis = computed(() => Array.isArray(props.emojis) ? props.emojis : props.emojis.value);
|
||||||
|
|
||||||
const shown = ref(!!props.initialShown);
|
const shown = ref(!!props.initialShown);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -60,7 +60,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
|
||||||
<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
|
<XSection
|
||||||
|
v-for="category in customEmojiCategories"
|
||||||
|
:key="`custom:${category}`"
|
||||||
|
:initial-shown="false"
|
||||||
|
:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).map(e => `:${e.name}:`))"
|
||||||
|
@chosen="chosen"
|
||||||
|
>
|
||||||
|
{{ category || i18n.ts.other }}
|
||||||
|
</XSection>
|
||||||
</div>
|
</div>
|
||||||
<div v-once class="group">
|
<div v-once class="group">
|
||||||
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
|
||||||
|
@ -88,7 +96,7 @@ import { deviceKind } from '@/scripts/device-kind';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis';
|
import { customEmojiCategories, customEmojis } from '@/custom-emojis';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
|
@ -104,7 +112,6 @@ const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const customEmojiCategories = getCustomEmojiCategories();
|
|
||||||
const searchEl = shallowRef<HTMLInputElement>();
|
const searchEl = shallowRef<HTMLInputElement>();
|
||||||
const emojisEl = shallowRef<HTMLDivElement>();
|
const emojisEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -138,7 +145,7 @@ watch(q, () => {
|
||||||
|
|
||||||
const searchCustom = () => {
|
const searchCustom = () => {
|
||||||
const max = 8;
|
const max = 8;
|
||||||
const emojis = customEmojis;
|
const emojis = customEmojis.value;
|
||||||
const matches = new Set<Misskey.entities.CustomEmoji>();
|
const matches = new Set<Misskey.entities.CustomEmoji>();
|
||||||
|
|
||||||
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
const exactMatch = emojis.find(emoji => emoji.name === newQ);
|
||||||
|
@ -323,7 +330,7 @@ function done(query?: string): boolean | void {
|
||||||
if (query == null || typeof query !== 'string') return;
|
if (query == null || typeof query !== 'string') return;
|
||||||
|
|
||||||
const q2 = query.replace(/:/g, '');
|
const q2 = query.replace(/:/g, '');
|
||||||
const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2);
|
const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
|
||||||
if (exactMatchCustom) {
|
if (exactMatchCustom) {
|
||||||
chosen(exactMatchCustom);
|
chosen(exactMatchCustom);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -335,8 +335,7 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: 5px;
|
margin-right: 8px;
|
||||||
width: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.caret {
|
.caret {
|
||||||
|
|
|
@ -6,15 +6,15 @@
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<template v-for="(item, i) in group.items">
|
<template v-for="(item, i) in group.items">
|
||||||
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||||
<i v-if="item.icon" class="icon ti-fw" :class="item.icon"></i>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</a>
|
</a>
|
||||||
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
|
||||||
<i v-if="item.icon" class="icon ti-fw" :class="item.icon"></i>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</button>
|
</button>
|
||||||
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
|
||||||
<i v-if="item.icon" class="icon ti-fw" :class="item.icon"></i>
|
<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
|
||||||
<span class="text">{{ item.text }}</span>
|
<span class="text">{{ item.text }}</span>
|
||||||
</MkA>
|
</MkA>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<span v-if="isCustom && errored">:{{ customEmojiName }}:</span>
|
<span v-if="isCustom && errored">:{{ customEmojiName }}:</span>
|
||||||
<img v-else-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true"/>
|
<img v-else-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
|
||||||
<img v-else-if="char && !useOsNativeEmojis" :class="$style.root" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/>
|
<img v-else-if="char && !useOsNativeEmojis" :class="$style.root" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/>
|
||||||
<span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span>
|
<span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span>
|
||||||
<span v-else>{{ emoji }}</span>
|
<span v-else>{{ emoji }}</span>
|
||||||
|
@ -25,29 +25,29 @@ const props = defineProps<{
|
||||||
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||||
|
|
||||||
const isCustom = computed(() => props.emoji.startsWith(':'));
|
const isCustom = computed(() => props.emoji.startsWith(':'));
|
||||||
const customEmojiName = props.emoji.substr(1, props.emoji.length - 2).replace('@.', '');
|
const customEmojiName = computed(() => props.emoji.substr(1, props.emoji.length - 2).replace('@.', ''));
|
||||||
const char = computed(() => isCustom.value ? undefined : props.emoji);
|
const char = computed(() => isCustom.value ? undefined : props.emoji);
|
||||||
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction);
|
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction);
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
if (char.value) {
|
if (char.value) {
|
||||||
return char2path(char.value);
|
return char2path(char.value);
|
||||||
} else if (props.host == null && !customEmojiName.includes('@')) {
|
} else if (props.host == null && !customEmojiName.value.includes('@')) {
|
||||||
const found = customEmojis.find(x => x.name === customEmojiName);
|
const found = customEmojis.value.find(x => x.name === customEmojiName.value);
|
||||||
return found ? found.url : null;
|
return found ? defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(found.url) : found.url : null;
|
||||||
} else {
|
} else {
|
||||||
const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`;
|
const rawUrl = props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
|
||||||
return defaultStore.state.disableShowingAnimatedImages
|
return defaultStore.state.disableShowingAnimatedImages
|
||||||
? getStaticImageUrl(rawUrl)
|
? getStaticImageUrl(rawUrl)
|
||||||
: rawUrl;
|
: rawUrl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value);
|
const alt = computed(() => isCustom.value ? `:${customEmojiName.value}:` : char.value);
|
||||||
let errored = $ref(isCustom.value && url.value == null);
|
let errored = $ref(isCustom.value && url.value == null);
|
||||||
|
|
||||||
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
||||||
function computeTitle(event: PointerEvent): void {
|
function computeTitle(event: PointerEvent): void {
|
||||||
const title = isCustom.value
|
const title = isCustom.value
|
||||||
? `:${customEmojiName}:`
|
? `:${customEmojiName.value}:`
|
||||||
: (getEmojiName(char.value as string) ?? char.value as string);
|
: (getEmojiName(char.value as string) ?? char.value as string);
|
||||||
(event.target as HTMLElement).title = title;
|
(event.target as HTMLElement).title = title;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,51 @@
|
||||||
import { api } from './os';
|
import { shallowRef, computed, markRaw } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { apiGet } from './os';
|
||||||
import { miLocalStorage } from './local-storage';
|
import { miLocalStorage } from './local-storage';
|
||||||
|
import { stream } from '@/stream';
|
||||||
|
|
||||||
const storageCache = miLocalStorage.getItem('emojis');
|
const storageCache = miLocalStorage.getItem('emojis');
|
||||||
export let customEmojis: {
|
export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
|
||||||
name: string;
|
export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
|
||||||
aliases: string[];
|
const categories = new Set<string>();
|
||||||
category: string;
|
for (const emoji of customEmojis.value) {
|
||||||
url: string;
|
if (emoji.category && emoji.category !== 'null') {
|
||||||
}[] = storageCache ? JSON.parse(storageCache) : [];
|
categories.add(emoji.category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markRaw([...Array.from(categories), null]);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('emojiAdded', emojiData => {
|
||||||
|
customEmojis.value = [emojiData.emoji, ...customEmojis.value];
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('emojiUpdated', emojiData => {
|
||||||
|
customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.CustomEmoji ?? item);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('emojiDeleted', emojiData => {
|
||||||
|
customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name));
|
||||||
|
});
|
||||||
|
|
||||||
export async function fetchCustomEmojis() {
|
export async function fetchCustomEmojis() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt');
|
const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt');
|
||||||
if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60 * 24) return;
|
if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return;
|
||||||
|
|
||||||
const res = await api('emojis', {});
|
const res = await apiGet('emojis', {});
|
||||||
|
|
||||||
customEmojis = res.emojis;
|
customEmojis.value = res.emojis;
|
||||||
miLocalStorage.setItem('emojis', JSON.stringify(customEmojis));
|
miLocalStorage.setItem('emojis', JSON.stringify(res.emojis));
|
||||||
miLocalStorage.setItem('lastEmojisFetchedAt', now.toString());
|
miLocalStorage.setItem('lastEmojisFetchedAt', now.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedCategories;
|
|
||||||
export function getCustomEmojiCategories() {
|
|
||||||
if (cachedCategories) return cachedCategories;
|
|
||||||
|
|
||||||
const categories = new Set();
|
|
||||||
for (const emoji of customEmojis) {
|
|
||||||
categories.add(emoji.category);
|
|
||||||
}
|
|
||||||
const res = Array.from(categories);
|
|
||||||
cachedCategories = res;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedTags;
|
let cachedTags;
|
||||||
export function getCustomEmojiTags() {
|
export function getCustomEmojiTags() {
|
||||||
if (cachedTags) return cachedTags;
|
if (cachedTags) return cachedTags;
|
||||||
|
|
||||||
const tags = new Set();
|
const tags = new Set();
|
||||||
for (const emoji of customEmojis) {
|
for (const emoji of customEmojis.value) {
|
||||||
for (const tag of emoji.aliases) {
|
for (const tag of emoji.aliases) {
|
||||||
tags.add(tag);
|
tags.add(tag);
|
||||||
}
|
}
|
||||||
|
|
|
@ -338,11 +338,6 @@ import { fetchCustomEmojis } from './custom-emojis';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('emojiAdded', emojiData => {
|
|
||||||
// TODO
|
|
||||||
//store.commit('instance/set', );
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
||||||
import('./plugin').then(({ install }) => {
|
import('./plugin').then(({ install }) => {
|
||||||
install(plugin);
|
install(plugin);
|
||||||
|
|
|
@ -39,13 +39,13 @@ import MkSelect from '@/components/MkSelect.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkTab from '@/components/MkTab.vue';
|
import MkTab from '@/components/MkTab.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { customEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis';
|
import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
|
||||||
const customEmojiCategories = getCustomEmojiCategories();
|
|
||||||
const customEmojiTags = getCustomEmojiTags();
|
const customEmojiTags = getCustomEmojiTags();
|
||||||
let q = $ref('');
|
let q = $ref('');
|
||||||
let searchEmojis = $ref(null);
|
let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null);
|
||||||
let selectedTags = $ref(new Set());
|
let selectedTags = $ref(new Set());
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
|
@ -55,9 +55,9 @@ function search() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedTags.size === 0) {
|
if (selectedTags.size === 0) {
|
||||||
searchEmojis = customEmojis.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q));
|
searchEmojis = customEmojis.value.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q));
|
||||||
} else {
|
} else {
|
||||||
searchEmojis = customEmojis.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<MkInput v-model="name">
|
<MkInput v-model="name">
|
||||||
<template #label>{{ i18n.ts.name }}</template>
|
<template #label>{{ i18n.ts.name }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="category" :datalist="categories">
|
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||||
<template #label>{{ i18n.ts.category }}</template>
|
<template #label>{{ i18n.ts.category }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-model="aliases">
|
<MkInput v-model="aliases">
|
||||||
|
@ -36,7 +36,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { unique } from '@/scripts/array';
|
import { unique } from '@/scripts/array';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { getCustomEmojiCategories } from '@/custom-emojis';
|
import { customEmojiCategories } from '@/custom-emojis';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emoji: any,
|
emoji: any,
|
||||||
|
@ -46,7 +46,6 @@ let dialog = $ref(null);
|
||||||
let name: string = $ref(props.emoji.name);
|
let name: string = $ref(props.emoji.name);
|
||||||
let category: string = $ref(props.emoji.category);
|
let category: string = $ref(props.emoji.category);
|
||||||
let aliases: string = $ref(props.emoji.aliases.join(' '));
|
let aliases: string = $ref(props.emoji.aliases.join(' '));
|
||||||
const categories = getCustomEmojiCategories();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
|
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
|
||||||
|
|
|
@ -313,7 +313,7 @@ let preview_mention = $ref('@example');
|
||||||
let preview_hashtag = $ref('#test');
|
let preview_hashtag = $ref('#test');
|
||||||
let preview_url = $ref('https://example.com');
|
let preview_url = $ref('https://example.com');
|
||||||
let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
|
let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
|
||||||
let preview_emoji = $ref(customEmojis.length ? `:${customEmojis[0].name}:` : ':emojiname:');
|
let preview_emoji = $ref(customEmojis.value.length ? `:${customEmojis.value[0].name}:` : ':emojiname:');
|
||||||
let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
|
let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
|
||||||
let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
|
let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
|
||||||
let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
|
let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
|
||||||
|
|
|
@ -10,7 +10,7 @@ export function createAiScriptEnv(opts) {
|
||||||
USER_ID: $i ? values.STR($i.id) : values.NULL,
|
USER_ID: $i ? values.STR($i.id) : values.NULL,
|
||||||
USER_NAME: $i ? values.STR($i.name) : values.NULL,
|
USER_NAME: $i ? values.STR($i.name) : values.NULL,
|
||||||
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
|
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
|
||||||
CUSTOM_EMOJIS: utils.jsToVal(customEmojis),
|
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
|
||||||
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
|
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
|
||||||
await os.alert({
|
await os.alert({
|
||||||
type: type ? type.value : 'info',
|
type: type ? type.value : 'info',
|
||||||
|
|
Loading…
Reference in a new issue