Merge tag 'tags/2024.3.3'
This commit is contained in:
commit
df14b213b4
91 changed files with 432 additions and 214 deletions
|
@ -88,7 +88,7 @@
|
|||
"@smithy/node-http-handler": "2.1.10",
|
||||
"@swc/cli": "0.1.63",
|
||||
"@swc/core": "1.3.107",
|
||||
"@transfem-org/sfm-js": "0.24.4",
|
||||
"@transfem-org/sfm-js": "0.24.5",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
|
@ -172,7 +172,7 @@
|
|||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.22.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.2",
|
||||
"tmp": "0.2.3",
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.20",
|
||||
|
|
|
@ -430,11 +430,16 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
update.hasPoll = !!data.poll;
|
||||
}
|
||||
|
||||
// technically we should check if the two sets of files are
|
||||
// different, or if their descriptions have changed. In practice
|
||||
// this is good enough.
|
||||
const filesChanged = oldnote.fileIds?.length || data.files?.length;
|
||||
|
||||
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
|
||||
|
||||
const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null;
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
if (Object.keys(update).length > 0 || filesChanged) {
|
||||
const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id });
|
||||
|
||||
await this.noteEditRepository.insert({
|
||||
|
|
|
@ -64,8 +64,8 @@ type DecodedReaction = {
|
|||
host?: string | null;
|
||||
};
|
||||
|
||||
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
|
||||
const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
||||
const isCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@\.)?:$/u;
|
||||
const decodeCustomEmojiRegexp = /^:([\p{Letter}\p{Number}\p{Mark}_+-]+)(?:@([\w.-]+))?:$/u;
|
||||
|
||||
@Injectable()
|
||||
export class ReactionService {
|
||||
|
|
|
@ -31,6 +31,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { MetaService } from '../MetaService.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import { CONTEXT } from './misc/contexts.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
|
||||
@Injectable()
|
||||
|
@ -283,9 +284,10 @@ export class ApRendererService {
|
|||
if (instance && instance.softwareName === 'mastodon') isMastodon = true;
|
||||
if (instance && instance.softwareName === 'akkoma') isMastodon = true;
|
||||
if (instance && instance.softwareName === 'pleroma') isMastodon = true;
|
||||
if (instance && instance.softwareName === 'iceshrimp.net') isMastodon = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const object: ILike = {
|
||||
type: 'Like',
|
||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
||||
|
@ -785,48 +787,7 @@ export class ApRendererService {
|
|||
x.id = `${this.config.url}/${randomUUID()}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
Key: 'sec:Key',
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
fedibird: 'http://fedibird.com/ns#',
|
||||
quoteUri: 'fedibird:quoteUri',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: 'https://misskey-hub.net/ns#',
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'isCat': 'misskey:isCat',
|
||||
// Firefish
|
||||
firefish: 'https://joinfirefish.org/ns#',
|
||||
speakAsCat: 'firefish:speakAsCat',
|
||||
// Sharkey
|
||||
sharkey: 'https://joinsharkey.org/ns#',
|
||||
backgroundUrl: 'sharkey:backgroundUrl',
|
||||
listenbrainz: 'sharkey:listenbrainz',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
},
|
||||
],
|
||||
}, x as T & { id: string });
|
||||
return Object.assign({ '@context': CONTEXT }, x as T & { id: string });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as crypto from 'node:crypto';
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CONTEXTS } from './misc/contexts.js';
|
||||
import { CONTEXT, CONTEXTS } from './misc/contexts.js';
|
||||
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
||||
import type { JsonLdDocument } from 'jsonld';
|
||||
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||
|
@ -88,6 +88,16 @@ class LdSignature {
|
|||
return verifyData;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async compact(data: any, context: any = CONTEXT): Promise<JsonLdDocument> {
|
||||
const customLoader = this.getLoader();
|
||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||
return (await import('jsonld')).default.compact(data, context, {
|
||||
documentLoader: customLoader,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||
const customLoader = this.getLoader();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { JsonLd } from 'jsonld/jsonld-spec.js';
|
||||
import type { Context, JsonLd } from 'jsonld/jsonld-spec.js';
|
||||
|
||||
/* eslint:disable:quotemark indent */
|
||||
const id_v1 = {
|
||||
|
@ -526,6 +526,50 @@ const activitystreams = {
|
|||
},
|
||||
} satisfies JsonLd;
|
||||
|
||||
const context_iris = [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
];
|
||||
|
||||
const extension_context_definition = {
|
||||
Key: 'sec:Key',
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
fedibird: 'http://fedibird.com/ns#',
|
||||
quoteUri: 'fedibird:quoteUri',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: 'https://misskey-hub.net/ns#',
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'isCat': 'misskey:isCat',
|
||||
// Firefish
|
||||
firefish: 'https://joinfirefish.org/ns#',
|
||||
speakAsCat: 'firefish:speakAsCat',
|
||||
// Sharkey
|
||||
sharkey: 'https://joinsharkey.org/ns#',
|
||||
backgroundUrl: 'sharkey:backgroundUrl',
|
||||
listenbrainz: 'sharkey:listenbrainz',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
} satisfies Context;
|
||||
|
||||
export const CONTEXT: (string | Context)[] = [...context_iris, extension_context_definition];
|
||||
|
||||
export const CONTEXTS: Record<string, JsonLd> = {
|
||||
'https://w3id.org/identity/v1': id_v1,
|
||||
'https://w3id.org/security/v1': security_v1,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { Not, IsNull, DataSource } from 'typeorm';
|
||||
import { Not, IsNull, Like, DataSource } from 'typeorm';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -37,7 +37,10 @@ export default class UsersChart extends Chart<typeof schema> { // eslint-disable
|
|||
|
||||
protected async tickMajor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
const [localCount, remoteCount] = await Promise.all([
|
||||
this.usersRepository.countBy({ host: IsNull() }),
|
||||
// that Not(Like()) is ugly, but it matches the logic in
|
||||
// packages/backend/src/models/User.ts to not count "system"
|
||||
// accounts
|
||||
this.usersRepository.countBy({ host: IsNull(), username: Not(Like('%.%')) }),
|
||||
this.usersRepository.countBy({ host: Not(IsNull()) }),
|
||||
]);
|
||||
|
||||
|
|
|
@ -33,6 +33,12 @@ export class CleanRemoteFilesProcessorService {
|
|||
|
||||
let deletedCount = 0;
|
||||
let cursor: MiDriveFile['id'] | null = null;
|
||||
let errorCount = 0;
|
||||
|
||||
const total = await this.driveFilesRepository.countBy({
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const files = await this.driveFilesRepository.find({
|
||||
|
@ -41,7 +47,7 @@ export class CleanRemoteFilesProcessorService {
|
|||
isLink: false,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 8,
|
||||
take: 256,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
|
@ -54,18 +60,22 @@ export class CleanRemoteFilesProcessorService {
|
|||
|
||||
cursor = files.at(-1)?.id ?? null;
|
||||
|
||||
await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true)));
|
||||
// Handle deletion in a batch
|
||||
const results = await Promise.allSettled(files.map(file => this.driveService.deleteFileSync(file, true)));
|
||||
|
||||
deletedCount += 8;
|
||||
|
||||
const total = await this.driveFilesRepository.countBy({
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
deletedCount++;
|
||||
} else {
|
||||
this.logger.error(`Failed to delete file ID ${files[index].id}: ${result.reason}`);
|
||||
errorCount++;
|
||||
}
|
||||
});
|
||||
|
||||
job.updateProgress(deletedCount / total);
|
||||
await job.updateProgress(100 / total * deletedCount);
|
||||
|
||||
}
|
||||
|
||||
this.logger.succ('All cached remote files has been deleted.');
|
||||
this.logger.succ(`All cached remote files processed. Total deleted: ${deletedCount}, Failed: ${errorCount}.`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ export class ExportCustomEmojisProcessorService {
|
|||
});
|
||||
|
||||
for (const emoji of customEmojis) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
|
||||
if (!/^[\p{Letter}\p{Number}\p{Mark}_+-]+$/u.test(emoji.name)) {
|
||||
this.logger.error(`invalid emoji name: ${emoji.name}`);
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -79,13 +79,14 @@ export class ImportCustomEmojisProcessorService {
|
|||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(emojiInfo.name)) {
|
||||
this.logger.error(`invalid emojiname: ${emojiInfo.name}`);
|
||||
const nameNfc = emojiInfo.name.normalize('NFC');
|
||||
if (!/^[\p{Letter}\p{Number}\p{Mark}_+-]+$/u.test(nameNfc)) {
|
||||
this.logger.error(`invalid emojiname: ${nameNfc}`);
|
||||
continue;
|
||||
}
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await this.emojisRepository.delete({
|
||||
name: emojiInfo.name,
|
||||
name: nameNfc,
|
||||
});
|
||||
const driveFile = await this.driveService.addFile({
|
||||
user: null,
|
||||
|
@ -94,10 +95,10 @@ export class ImportCustomEmojisProcessorService {
|
|||
force: true,
|
||||
});
|
||||
await this.customEmojiService.add({
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
name: nameNfc,
|
||||
category: emojiInfo.category?.normalize('NFC'),
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')),
|
||||
driveFile,
|
||||
license: emojiInfo.license,
|
||||
isSensitive: emojiInfo.isSensitive,
|
||||
|
|
|
@ -15,6 +15,7 @@ import InstanceChart from '@/core/chart/charts/instance.js';
|
|||
import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||
import FederationChart from '@/core/chart/charts/federation.js';
|
||||
import { getApId } from '@/core/activitypub/type.js';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
||||
|
@ -52,7 +53,7 @@ export class InboxProcessorService {
|
|||
@bindThis
|
||||
public async process(job: Bull.Job<InboxJobData>): Promise<string> {
|
||||
const signature = job.data.signature; // HTTP-signature
|
||||
const activity = job.data.activity;
|
||||
let activity = job.data.activity;
|
||||
|
||||
//#region Log
|
||||
const info = Object.assign({}, activity);
|
||||
|
@ -154,6 +155,17 @@ export class InboxProcessorService {
|
|||
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||
}
|
||||
|
||||
// アクティビティを正規化
|
||||
delete activity.signature;
|
||||
try {
|
||||
activity = await ldSignature.compact(activity) as IActivity;
|
||||
} catch (e) {
|
||||
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
|
||||
}
|
||||
// TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
|
||||
// https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
|
||||
activity.signature = ldSignature;
|
||||
|
||||
// もう一度actorチェック
|
||||
if (authUser.user.uri !== activity.actor) {
|
||||
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
|
||||
|
|
|
@ -192,6 +192,7 @@ export class FileServerService {
|
|||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
reply.code(206);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
|
@ -261,7 +262,6 @@ export class FileServerService {
|
|||
const parts = range.replace(/bytes=/, '').split('-');
|
||||
const start = parseInt(parts[0], 10);
|
||||
let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1;
|
||||
console.log(end);
|
||||
if (end > file.file.size) {
|
||||
end = file.file.size - 1;
|
||||
}
|
||||
|
@ -431,6 +431,7 @@ export class FileServerService {
|
|||
reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`);
|
||||
reply.header('Accept-Ranges', 'bytes');
|
||||
reply.header('Content-Length', chunksize);
|
||||
reply.code(206);
|
||||
} else {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
|
@ -527,6 +528,9 @@ export class FileServerService {
|
|||
if (!file.storedInternal) {
|
||||
if (!(file.isLink && file.uri)) return '204';
|
||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||
if (!file.size) {
|
||||
file.size = (await fs.promises.stat(result.path)).size;
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
url: file.uri,
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases);
|
||||
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export const meta = {
|
|||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
name: { type: 'string', pattern: '^[\\p{Letter}\\p{Number}\\p{Mark}_+-]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
|
@ -74,18 +74,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private emojiEntityService: EmojiEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const nameNfc = ps.name.normalize('NFC');
|
||||
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
|
||||
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
|
||||
|
||||
if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null });
|
||||
|
||||
const emoji = await this.customEmojiService.add({
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
name: nameNfc,
|
||||
category: ps.category?.normalize('NFC') ?? null,
|
||||
aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [],
|
||||
host: null,
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
|
|
|
@ -83,15 +83,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError();
|
||||
}
|
||||
|
||||
const nameNfc = emoji.name.normalize('NFC');
|
||||
// Duplication Check
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(emoji.name);
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
|
||||
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
|
||||
|
||||
const addedEmoji = await this.customEmojiService.add({
|
||||
driveFile,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
aliases: emoji.aliases,
|
||||
name: nameNfc,
|
||||
category: emoji.category?.normalize('NFC'),
|
||||
aliases: emoji.aliases?.map(a => a.normalize('NFC')),
|
||||
host: null,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
|
|
|
@ -99,7 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}
|
||||
|
||||
if (ps.query) {
|
||||
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query) + '%' })
|
||||
q.andWhere('emoji.name like :query', { query: '%' + sqlLikeEscape(ps.query.normalize('NFC')) + '%' })
|
||||
.orderBy('length(emoji.name)', 'ASC');
|
||||
}
|
||||
|
||||
|
|
|
@ -93,17 +93,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
//const emojis = await q.limit(ps.limit).getMany();
|
||||
|
||||
emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany();
|
||||
const queryarry = ps.query.match(/\:([a-z0-9_]*)\:/g);
|
||||
const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug);
|
||||
|
||||
if (queryarry) {
|
||||
emojis = emojis.filter(emoji =>
|
||||
queryarry.includes(`:${emoji.name}:`),
|
||||
queryarry.includes(`:${emoji.name.normalize('NFC')}:`),
|
||||
);
|
||||
} else {
|
||||
const queryNfc = ps.query!.normalize('NFC');
|
||||
emojis = emojis.filter(emoji =>
|
||||
emoji.name.includes(ps.query!) ||
|
||||
emoji.aliases.some(a => a.includes(ps.query!)) ||
|
||||
emoji.category?.includes(ps.query!));
|
||||
emoji.name.includes(queryNfc) ||
|
||||
emoji.aliases.some(a => a.includes(queryNfc)) ||
|
||||
emoji.category?.includes(queryNfc));
|
||||
}
|
||||
emojis.splice(ps.limit + 1);
|
||||
} else {
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases);
|
||||
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases);
|
||||
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category ?? null);
|
||||
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
name: { type: 'string', pattern: '^[\\p{Letter}\\p{Number}\\p{Mark}_+-]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
|
@ -73,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const nameNfc = ps.name?.normalize('NFC');
|
||||
let driveFile;
|
||||
if (ps.fileId) {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
@ -84,22 +85,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
emojiId = ps.id;
|
||||
const emoji = await this.customEmojiService.getEmojiById(ps.id);
|
||||
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
if (ps.name && (ps.name !== emoji.name)) {
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
||||
if (nameNfc && (nameNfc !== emoji.name)) {
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
|
||||
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
|
||||
}
|
||||
} else {
|
||||
if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
|
||||
const emoji = await this.customEmojiService.getEmojiByName(ps.name);
|
||||
if (!nameNfc) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
|
||||
const emoji = await this.customEmojiService.getEmojiByName(nameNfc);
|
||||
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
emojiId = emoji.id;
|
||||
}
|
||||
|
||||
await this.customEmojiService.update(emojiId, {
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category,
|
||||
aliases: ps.aliases,
|
||||
name: nameNfc,
|
||||
category: ps.category?.normalize('NFC'),
|
||||
aliases: ps.aliases?.map(a => a.normalize('NFC')),
|
||||
license: ps.license,
|
||||
isSensitive: ps.isSensitive,
|
||||
localOnly: ps.localOnly,
|
||||
|
|
|
@ -83,7 +83,7 @@ export class MastoConverters {
|
|||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
|
||||
public encodeFile(f: any): Entity.Attachment {
|
||||
return {
|
||||
id: f.id,
|
||||
|
@ -278,8 +278,9 @@ export class MastoConverters {
|
|||
reactions: status.emoji_reactions,
|
||||
emoji_reactions: status.emoji_reactions,
|
||||
bookmarked: false,
|
||||
quote: isQuote ? await this.convertReblog(status.reblog) : false,
|
||||
edited_at: note.updatedAt?.toISOString(),
|
||||
quote: isQuote ? await this.convertReblog(status.reblog) : null,
|
||||
// optional chaining cannot be used, as it evaluates to undefined, not null
|
||||
edited_at: note.updatedAt ? note.updatedAt.toISOString() : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,6 @@ html
|
|||
link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css')
|
||||
link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
script(src='/client-assets/libopenmpt.js')
|
||||
|
||||
if !config.clientManifestExists
|
||||
script(type="module" src="/vite/@vite/client")
|
||||
|
@ -73,7 +72,6 @@ html
|
|||
script.
|
||||
var VERSION = "#{version}";
|
||||
var CLIENT_ENTRY = "#{clientEntry.file}";
|
||||
window.libopenmpt = window.Module;
|
||||
|
||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||
!= metaJson
|
||||
|
|
|
@ -5,8 +5,8 @@ block vars
|
|||
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
- const images = note.cw ? [] : (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const videos = note.cw ? [] : (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -21,12 +21,12 @@
|
|||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@rollup/pluginutils": "5.1.0",
|
||||
"@transfem-org/sfm-js": "0.24.4",
|
||||
"@syuilo/aiscript": "0.17.0",
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@transfem-org/sfm-js": "0.24.5",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/compiler-sfc": "3.4.21",
|
||||
|
|
|
@ -238,7 +238,7 @@ function exec() {
|
|||
return;
|
||||
}
|
||||
|
||||
emojis.value = searchEmoji(props.q.toLowerCase(), emojiDb.value);
|
||||
emojis.value = searchEmoji(props.q.normalize('NFC').toLowerCase(), emojiDb.value);
|
||||
} else if (props.type === 'mfmTag') {
|
||||
if (!props.q || props.q === '') {
|
||||
mfmTags.value = MFM_TAGS;
|
||||
|
|
|
@ -205,7 +205,7 @@ watch(q, () => {
|
|||
return;
|
||||
}
|
||||
|
||||
const newQ = q.value.replace(/:/g, '').toLowerCase();
|
||||
const newQ = q.value.replace(/:/g, '').normalize('NFC').toLowerCase();
|
||||
|
||||
const searchCustom = () => {
|
||||
const max = 100;
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination" :displayLimit="50" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||
<MkA
|
||||
v-for="file in (items as Misskey.entities.DriveFile[])"
|
||||
:key="file.id"
|
||||
|
|
|
@ -55,8 +55,6 @@ import { i18n } from '@/i18n.js';
|
|||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
console.log(defaultStore.state.noteDesign, defaultStore.state.noteDesign === 'sharkey');
|
||||
|
||||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
|
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header>
|
||||
<template v-if="pageMetadata">
|
||||
<i v-if="pageMetadata.icon" :class="pageMetadata.icon" style="margin-right: 0.5em;"></i>
|
||||
<span>{{ pageMetadata.title }}</span>
|
||||
<span><MkUserName v-if="pageMetadata.userName?.name" :user="pageMetadata.userName" />{{ pageMetadata.title }}</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
@ -43,6 +43,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
|||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { useRouterFactory } from '@/router/supplier.js';
|
||||
import { mainRouter } from '@/router/main.js';
|
||||
import MkUserName from './global/MkUserName.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
|
|
|
@ -395,10 +395,10 @@ const prepend = (item: MisskeyEntity): void => {
|
|||
* @param newItems 新しいアイテムの配列
|
||||
*/
|
||||
function unshiftItems(newItems: MisskeyEntity[]) {
|
||||
const length = newItems.length + items.value.size;
|
||||
items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, props.displayLimit));
|
||||
|
||||
if (length >= props.displayLimit) more.value = true;
|
||||
const prevLength = items.value.size;
|
||||
items.value = new Map([...arrayToEntries(newItems), ...items.value].slice(0, newItems.length + props.displayLimit));
|
||||
// if we truncated, mark that there are more values to fetch
|
||||
if (items.value.size < prevLength) more.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -406,10 +406,10 @@ function unshiftItems(newItems: MisskeyEntity[]) {
|
|||
* @param oldItems 古いアイテムの配列
|
||||
*/
|
||||
function concatItems(oldItems: MisskeyEntity[]) {
|
||||
const length = oldItems.length + items.value.size;
|
||||
items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, props.displayLimit));
|
||||
|
||||
if (length >= props.displayLimit) more.value = true;
|
||||
const prevLength = items.value.size;
|
||||
items.value = new Map([...items.value, ...arrayToEntries(oldItems)].slice(0, oldItems.length + props.displayLimit));
|
||||
// if we truncated, mark that there are more values to fetch
|
||||
if (items.value.size < prevLength) more.value = true;
|
||||
}
|
||||
|
||||
function executeQueue() {
|
||||
|
@ -418,7 +418,7 @@ function executeQueue() {
|
|||
}
|
||||
|
||||
function prependQueue(newItem: MisskeyEntity) {
|
||||
queue.value = new Map([[newItem.id, newItem], ...queue.value].slice(0, props.displayLimit) as [string, MisskeyEntity][]);
|
||||
queue.value = new Map([[newItem.id, newItem], ...queue.value] as [string, MisskeyEntity][]);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { computed, ref } from 'vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sanitizeHtml from '@/scripts/sanitize-html.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
|
|
@ -16,9 +16,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-else-if="phase === 'howToReact'" class="_gaps">
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
|
||||
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
||||
<I18n :src="i18n.ts._initialTutorial._reaction.letsTryReacting" tag="div">
|
||||
<template #reaction>
|
||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||
</template>
|
||||
</I18n>
|
||||
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
|
||||
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
|
||||
<div v-if="onceReacted">
|
||||
<b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>
|
||||
<I18n :src="i18n.ts._initialTutorial._reaction.reactDone">
|
||||
<template #undo>
|
||||
<i class="ph-minus ph-bold ph-lg"></i>
|
||||
</template>
|
||||
</I18n>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkPagination :pagination="pagination">
|
||||
<MkPagination :pagination="pagination" :displayLimit="50">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
|
|
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sanitizeHtml from '@/scripts/sanitize-html.js';
|
||||
import XSigninDialog from '@/components/MkSigninDialog.vue';
|
||||
import XSignupDialog from '@/components/MkSignupDialog.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
|
|
@ -85,6 +85,7 @@ const errored = ref(url.value == null);
|
|||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
ev.stopPropagation();
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: `:${props.name}:`,
|
||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick" v-on:click.stop/>
|
||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick" v-on:click.stop>{{ colorizedNativeEmoji }}</span>
|
||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -39,6 +39,7 @@ function computeTitle(event: PointerEvent): void {
|
|||
|
||||
function onClick(ev: MouseEvent) {
|
||||
if (props.menu) {
|
||||
ev.stopPropagation();
|
||||
os.popupMenu([{
|
||||
type: 'label',
|
||||
text: props.emoji,
|
||||
|
|
|
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.items">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.item" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
|
|
|
@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import sanitizeHtml from '@/scripts/sanitize-html.js';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XEmojis from './about.emojis.vue';
|
||||
|
|
|
@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
-->
|
||||
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
|
||||
<XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="900">
|
||||
<div class="_gaps_m">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<SkApprovalUser v-for="item in items" :key="item.id" :user="(item as any)" :onDeleted="deleted"/>
|
||||
|
|
|
@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</FormSplit>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.instances">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" :class="$style.instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
|
|
|
@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||
<MkPagination ref="pagingComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
|
||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);">
|
||||
<MkPagination v-slot="{items}" ref="logs" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
|
||||
<div class="_gaps_s">
|
||||
<XModLog v-for="item in items" :key="item.id" :log="item"/>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<MkButton primary rounded @click="assign"><i class="ph-plus ph-bold ph-lg"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
|
||||
<MkPagination :pagination="usersPagination">
|
||||
<MkPagination :pagination="usersPagination" :displayLimit="50">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination">
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<div :class="$style.users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" :class="$style.user" :to="`/admin/user/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
|
|
|
@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline @click="setLicenseBulk">Set License</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #label>{{ i18n.ts.host }}</template>
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
<MkPagination :pagination="remotePagination">
|
||||
<MkPagination :pagination="remotePagination" :displayLimit="50">
|
||||
<template #empty><span>{{ i18n.ts.noCustomEmojis }}</span></template>
|
||||
<template #default="{items}">
|
||||
<div class="ldhfsamy">
|
||||
|
@ -352,6 +352,7 @@ definePageMetadata(() => ({
|
|||
> .img {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
@ -398,6 +399,7 @@ definePageMetadata(() => ({
|
|||
> .img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
> .body {
|
||||
|
|
|
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
|
||||
<MkInput v-model="name" autocapitalize="off">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||
|
|
|
@ -16,9 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'"
|
||||
v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<MkNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</MkDateSeparatedList>
|
||||
<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'sharkey'"
|
||||
v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false">
|
||||
<SkNote :key="item.id" :note="item.note" :class="$style.note"/>
|
||||
</MkDateSeparatedList>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkSpacer>
|
||||
|
@ -28,10 +33,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import SkNote from '@/components/SkNote.vue';
|
||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites' as const,
|
||||
|
|
|
@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<MkSelect v-model="type">
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="following" v-if="hasSender">{{ i18n.ts.following }}</option>
|
||||
<option value="follower" v-if="hasSender">{{ i18n.ts.followers }}</option>
|
||||
<option value="mutualFollow" v-if="hasSender">{{ i18n.ts.mutualFollow }}</option>
|
||||
<option value="followingOrFollower" v-if="hasSender">{{ i18n.ts.followingOrFollower }}</option>
|
||||
<option value="list" v-if="hasSender">{{ i18n.ts.userList }}</option>
|
||||
<option v-if="hasSender" value="following">{{ i18n.ts.following }}</option>
|
||||
<option v-if="hasSender" value="follower">{{ i18n.ts.followers }}</option>
|
||||
<option v-if="hasSender" value="mutualFollow">{{ i18n.ts.mutualFollow }}</option>
|
||||
<option v-if="hasSender" value="followingOrFollower">{{ i18n.ts.followingOrFollower }}</option>
|
||||
<option v-if="hasSender" value="list">{{ i18n.ts.userList }}</option>
|
||||
<option value="never">{{ i18n.ts.none }}</option>
|
||||
</MkSelect>
|
||||
|
||||
|
|
|
@ -140,6 +140,7 @@ type Profile = {
|
|||
hot: Record<keyof typeof defaultStoreSaveKeys, unknown>;
|
||||
cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
|
||||
fontSize: string | null;
|
||||
lang: string | null;
|
||||
cornerRadius: string | null;
|
||||
useSystemFont: 't' | null;
|
||||
wallpaper: string | null;
|
||||
|
@ -198,6 +199,7 @@ function getSettings(): Profile['settings'] {
|
|||
hot,
|
||||
cold,
|
||||
fontSize: miLocalStorage.getItem('fontSize'),
|
||||
lang: miLocalStorage.getItem('lang'),
|
||||
cornerRadius: miLocalStorage.getItem('cornerRadius'),
|
||||
useSystemFont: miLocalStorage.getItem('useSystemFont') as 't' | null,
|
||||
wallpaper: miLocalStorage.getItem('wallpaper'),
|
||||
|
@ -313,6 +315,13 @@ async function applyProfile(id: string): Promise<void> {
|
|||
miLocalStorage.removeItem('fontSize');
|
||||
}
|
||||
|
||||
// lang
|
||||
if (settings.lang) {
|
||||
miLocalStorage.setItem('lang', settings.lang);
|
||||
} else {
|
||||
miLocalStorage.removeItem('lang');
|
||||
}
|
||||
|
||||
// cornerRadius
|
||||
if (settings.cornerRadius) {
|
||||
miLocalStorage.setItem('cornerRadius', settings.cornerRadius);
|
||||
|
|
|
@ -130,7 +130,7 @@ definePageMetadata(() => ({
|
|||
title: i18n.ts.user,
|
||||
icon: 'ph-user ph-bold ph-lg',
|
||||
...user.value ? {
|
||||
title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
|
||||
title: user.value.name ? ` (@${user.value.username})` : `@${user.value.username}`,
|
||||
subtitle: `@${getAcct(user.value)}`,
|
||||
userName: user.value,
|
||||
avatar: user.value,
|
||||
|
|
|
@ -99,7 +99,7 @@ export class Autocomplete {
|
|||
const isHashtag = hashtagIndex !== -1;
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' ');
|
||||
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[\p{Letter}\p{Number}\p{Mark}_+-]+:/u).pop()!.includes(':');
|
||||
|
||||
let opened = false;
|
||||
|
||||
|
@ -125,7 +125,7 @@ export class Autocomplete {
|
|||
if (isEmoji && !opened && this.onlyType.includes('emoji')) {
|
||||
const emoji = text.substring(emojiIndex + 1);
|
||||
if (!emoji.includes(' ')) {
|
||||
this.open('emoji', emoji);
|
||||
this.open('emoji', emoji.normalize('NFC'));
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
import type { Note, MeDetailed } from "misskey-js/entities.js";
|
||||
|
||||
export function checkWordMute(note: Note, me: MeDetailed | null | undefined, mutedWords: Array<string | string[]>): boolean {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
const text = getNoteText(note);
|
||||
|
||||
if (text === '') return false;
|
||||
|
||||
|
@ -40,3 +42,25 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNoteText(note: Note): string {
|
||||
const textParts: string[] = [];
|
||||
|
||||
if (note.cw)
|
||||
textParts.push(note.cw);
|
||||
|
||||
if (note.text)
|
||||
textParts.push(note.text);
|
||||
|
||||
if (note.files)
|
||||
for (const file of note.files)
|
||||
if (file.comment)
|
||||
textParts.push(file.comment);
|
||||
|
||||
if (note.poll)
|
||||
for (const choice of note.poll.choices)
|
||||
if (choice.text)
|
||||
textParts.push(choice.text);
|
||||
|
||||
return textParts.join('\n').trim();
|
||||
}
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
/* global libopenmpt UTF8ToString writeAsciiToMemory */
|
||||
/* eslint-disable */
|
||||
|
||||
const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
|
||||
export function ChiptuneJsConfig (repeatCount: number, context: AudioContext) {
|
||||
let libopenmpt
|
||||
let libopenmptLoadPromise
|
||||
|
||||
type ChiptuneJsConfig = {
|
||||
repeatCount: number | null;
|
||||
context: AudioContext | null;
|
||||
};
|
||||
|
||||
export function ChiptuneJsConfig (repeatCount?: number, context?: AudioContext) {
|
||||
this.repeatCount = repeatCount;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig;
|
||||
|
||||
export function ChiptuneJsPlayer (config: object) {
|
||||
export function ChiptuneJsPlayer (config: ChiptuneJsConfig) {
|
||||
this.config = config;
|
||||
this.audioContext = config.context || new ChiptuneAudioContext();
|
||||
this.context = this.audioContext.createGain();
|
||||
|
@ -20,6 +27,28 @@ export function ChiptuneJsPlayer (config: object) {
|
|||
this.volume = 1;
|
||||
}
|
||||
|
||||
ChiptuneJsPlayer.prototype.initialize = function() {
|
||||
if (libopenmptLoadPromise) return libopenmptLoadPromise;
|
||||
if (libopenmpt) return Promise.resolve();
|
||||
|
||||
libopenmptLoadPromise = new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
const { Module } = await import('./libopenmpt/libopenmpt.js');
|
||||
await new Promise((resolve) => {
|
||||
Module['onRuntimeInitialized'] = resolve;
|
||||
})
|
||||
libopenmpt = Module;
|
||||
resolve()
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
libopenmptLoadPromise = undefined;
|
||||
}
|
||||
})
|
||||
|
||||
return libopenmptLoadPromise;
|
||||
}
|
||||
|
||||
ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer;
|
||||
|
||||
ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) {
|
||||
|
@ -61,12 +90,12 @@ ChiptuneJsPlayer.prototype.seek = function (position: number) {
|
|||
|
||||
ChiptuneJsPlayer.prototype.metadata = function () {
|
||||
const data = {};
|
||||
const keys = UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
|
||||
const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';');
|
||||
let keyNameBuffer = 0;
|
||||
for (const key of keys) {
|
||||
keyNameBuffer = libopenmpt._malloc(key.length + 1);
|
||||
writeAsciiToMemory(key, keyNameBuffer);
|
||||
data[key] = UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
|
||||
libopenmpt.writeAsciiToMemory(key, keyNameBuffer);
|
||||
data[key] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer));
|
||||
libopenmpt._free(keyNameBuffer);
|
||||
}
|
||||
return data;
|
||||
|
@ -84,7 +113,7 @@ ChiptuneJsPlayer.prototype.unlock = function () {
|
|||
};
|
||||
|
||||
ChiptuneJsPlayer.prototype.load = function (input) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return this.initialize().then(() => new Promise((resolve, reject) => {
|
||||
if(this.touchLocked) {
|
||||
this.unlock();
|
||||
}
|
||||
|
@ -106,7 +135,7 @@ ChiptuneJsPlayer.prototype.load = function (input) {
|
|||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) {
|
||||
|
@ -180,7 +209,7 @@ ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) {
|
|||
|
||||
ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) {
|
||||
if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) {
|
||||
return UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
|
||||
return libopenmpt.UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true));
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
|
25
packages/frontend/src/scripts/libopenmpt/LICENSE
Normal file
25
packages/frontend/src/scripts/libopenmpt/LICENSE
Normal file
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2004-2024, OpenMPT Project Developers and Contributors
|
||||
Copyright (c) 1997-2003, Olivier Lapicque
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the OpenMPT project nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
8
packages/frontend/src/scripts/libopenmpt/libopenmpt.js
Normal file
8
packages/frontend/src/scripts/libopenmpt/libopenmpt.js
Normal file
File diff suppressed because one or more lines are too long
23
packages/frontend/src/scripts/libopenmpt/readme.md
Normal file
23
packages/frontend/src/scripts/libopenmpt/readme.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
modifications made to `libopenmpt.js` (can be taken from https://lib.openmpt.org/libopenmpt/download/):
|
||||
|
||||
at the beginning of the file:
|
||||
```js
|
||||
// @ts-nocheck
|
||||
/* eslint-disable */
|
||||
```
|
||||
|
||||
at the end of the file:
|
||||
```js
|
||||
Module.UTF8ToString = UTF8ToString;
|
||||
Module.writeAsciiToMemory = writeAsciiToMemory;
|
||||
export { Module }
|
||||
```
|
||||
|
||||
replace
|
||||
```
|
||||
wasmBinaryFile="libopenmpt.wasm"
|
||||
```
|
||||
with
|
||||
```
|
||||
wasmBinaryFile=new URL("./libopenmpt.wasm", import.meta.url).href
|
||||
```
|
|
@ -9,9 +9,9 @@ const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm;
|
|||
|
||||
function ifAfter(prefix, fn) {
|
||||
const preLen = prefix.length;
|
||||
const regex = new RegExp(prefix,'i');
|
||||
return (x,pos,string) => {
|
||||
return pos > 0 && string.substring(pos-preLen,pos).match(regex) ? fn(x) : x;
|
||||
const regex = new RegExp(prefix, 'i');
|
||||
return (x, pos, string) => {
|
||||
return pos > 0 && string.substring(pos - preLen, pos).match(regex) ? fn(x) : x;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ export function nyaize(text: string): string {
|
|||
.replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan'))
|
||||
// ko-KR
|
||||
.replace(koRegex1, match => String.fromCharCode(
|
||||
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
match.charCodeAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
|
||||
))
|
||||
.replace(koRegex2, '다냥')
|
||||
.replace(koRegex3, '냥');
|
||||
|
|
18
packages/frontend/src/scripts/sanitize-html.ts
Normal file
18
packages/frontend/src/scripts/sanitize-html.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: dakkar and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import original from 'sanitize-html';
|
||||
|
||||
export default function sanitizeHtml(str: string | null): string | null {
|
||||
if (str == null) return str;
|
||||
return original(str, {
|
||||
allowedTags: original.defaults.allowedTags.concat(['img', 'audio', 'video', 'center', 'details', 'summary']),
|
||||
allowedAttributes: {
|
||||
...original.defaults.allowedAttributes,
|
||||
a: original.defaults.allowedAttributes.a.concat(['style']),
|
||||
img: original.defaults.allowedAttributes.img.concat(['style']),
|
||||
},
|
||||
});
|
||||
}
|
|
@ -8,7 +8,7 @@ import meta from '../../package.json';
|
|||
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
|
||||
import pluginJson5 from './vite.json5.js';
|
||||
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
|
||||
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue', '.wasm'];
|
||||
|
||||
const hash = (str: string, seed = 0): number => {
|
||||
let h1 = 0xdeadbeef ^ seed,
|
||||
|
|
|
@ -19,6 +19,7 @@ namespace Entity {
|
|||
content: string
|
||||
plain_content?: string | null
|
||||
created_at: string
|
||||
edited_at: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
@ -38,7 +39,7 @@ namespace Entity {
|
|||
language: string | null
|
||||
pinned: boolean | null
|
||||
emoji_reactions: Array<Reaction>
|
||||
quote: Status | boolean
|
||||
quote: Status | boolean | null
|
||||
bookmarked: boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -725,6 +725,7 @@ namespace FriendicaAPI {
|
|||
content: s.content,
|
||||
plain_content: null,
|
||||
created_at: s.created_at,
|
||||
edited_at: s.edited_at || null,
|
||||
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
|
||||
replies_count: s.replies_count,
|
||||
reblogs_count: s.reblogs_count,
|
||||
|
|
|
@ -17,6 +17,7 @@ namespace FriendicaEntity {
|
|||
reblog: Status | null
|
||||
content: string
|
||||
created_at: string
|
||||
edited_at?: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
|
|
@ -628,6 +628,7 @@ namespace MastodonAPI {
|
|||
content: s.content,
|
||||
plain_content: null,
|
||||
created_at: s.created_at,
|
||||
edited_at: s.edited_at || null,
|
||||
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
|
||||
replies_count: s.replies_count,
|
||||
reblogs_count: s.reblogs_count,
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace MastodonEntity {
|
|||
reblog: Status | null
|
||||
content: string
|
||||
created_at: string
|
||||
edited_at?: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
|
|
@ -283,6 +283,7 @@ namespace MisskeyAPI {
|
|||
: '',
|
||||
plain_content: n.text ? n.text : null,
|
||||
created_at: n.createdAt,
|
||||
edited_at: n.updatedAt || null,
|
||||
emojis: mapEmojis(n.emojis).concat(mapReactionEmojis(n.reactionEmojis)),
|
||||
replies_count: n.repliesCount,
|
||||
reblogs_count: n.renoteCount,
|
||||
|
@ -303,7 +304,7 @@ namespace MisskeyAPI {
|
|||
pinned: null,
|
||||
emoji_reactions: typeof n.reactions === 'object' ? mapReactions(n.reactions, n.myReaction) : [],
|
||||
bookmarked: false,
|
||||
quote: n.renote && n.text ? note(n.renote, n.user.host ? n.user.host : host ? host : null) : false
|
||||
quote: n.renote && n.text ? note(n.renote, n.user.host ? n.user.host : host ? host : null) : null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace MisskeyEntity {
|
|||
export type Note = {
|
||||
id: string
|
||||
createdAt: string
|
||||
updatedAt?: string | null
|
||||
userId: string
|
||||
user: User
|
||||
text: string | null
|
||||
|
|
|
@ -357,6 +357,7 @@ namespace PleromaAPI {
|
|||
content: s.content,
|
||||
plain_content: s.pleroma.content?.['text/plain'] ? s.pleroma.content['text/plain'] : null,
|
||||
created_at: s.created_at,
|
||||
edited_at: s.edited_at || null,
|
||||
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
|
||||
replies_count: s.replies_count,
|
||||
reblogs_count: s.reblogs_count,
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace PleromaEntity {
|
|||
reblog: Status | null
|
||||
content: string
|
||||
created_at: string
|
||||
edited_at?: string | null
|
||||
emojis: Emoji[]
|
||||
replies_count: number
|
||||
reblogs_count: number
|
||||
|
|
|
@ -49,6 +49,7 @@ const status: Entity.Status = {
|
|||
content: 'hoge',
|
||||
plain_content: null,
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
edited_at: null,
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
|
|
|
@ -38,6 +38,7 @@ const status: Entity.Status = {
|
|||
content: 'hoge',
|
||||
plain_content: 'hoge',
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
edited_at: null,
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
|
|
|
@ -37,6 +37,7 @@ const status: Entity.Status = {
|
|||
content: 'hoge',
|
||||
plain_content: 'hoge',
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
edited_at: null,
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue