Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
		
						commit
						04bafc5aee
					
				
					 190 changed files with 3668 additions and 4909 deletions
				
			
		| 
						 | 
					@ -17,6 +17,8 @@
 | 
				
			||||||
- Chat UIが削除されました
 | 
					- Chat UIが削除されました
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Improvements
 | 
					### Improvements
 | 
				
			||||||
 | 
					- カスタム絵文字一括編集機能
 | 
				
			||||||
 | 
					- カスタム絵文字一括インポート
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Bugfixes
 | 
					### Bugfixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -180,6 +180,7 @@
 | 
				
			||||||
		"typeorm": "0.2.39",
 | 
							"typeorm": "0.2.39",
 | 
				
			||||||
		"typescript": "4.4.4",
 | 
							"typescript": "4.4.4",
 | 
				
			||||||
		"ulid": "2.3.0",
 | 
							"ulid": "2.3.0",
 | 
				
			||||||
 | 
							"unzipper": "0.10.11",
 | 
				
			||||||
		"uuid": "8.3.2",
 | 
							"uuid": "8.3.2",
 | 
				
			||||||
		"web-push": "3.4.5",
 | 
							"web-push": "3.4.5",
 | 
				
			||||||
		"websocket": "1.0.34",
 | 
							"websocket": "1.0.34",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Random avatar generator
 | 
					 * Identicon generator
 | 
				
			||||||
 | 
					 * https://en.wikipedia.org/wiki/Identicon
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as p from 'pureimage';
 | 
					import * as p from 'pureimage';
 | 
				
			||||||
| 
						 | 
					@ -34,9 +35,9 @@ const cellSize = actualSize / n;
 | 
				
			||||||
const sideN = Math.floor(n / 2);
 | 
					const sideN = Math.floor(n / 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Generate buffer of random avatar by seed
 | 
					 * Generate buffer of an identicon by seed
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function genAvatar(seed: string, stream: WriteStream): Promise<void> {
 | 
					export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
 | 
				
			||||||
	const rand = gen.create(seed);
 | 
						const rand = gen.create(seed);
 | 
				
			||||||
	const canvas = p.make(size, size);
 | 
						const canvas = p.make(size, size);
 | 
				
			||||||
	const ctx = canvas.getContext('2d');
 | 
						const ctx = canvas.getContext('2d');
 | 
				
			||||||
| 
						 | 
					@ -159,7 +159,7 @@ export class UserRepository extends Repository<User> {
 | 
				
			||||||
		if (user.avatarUrl) {
 | 
							if (user.avatarUrl) {
 | 
				
			||||||
			return user.avatarUrl;
 | 
								return user.avatarUrl;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return `${config.url}/random-avatar/${user.id}`;
 | 
								return `${config.url}/identicon/${user.id}`;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -213,6 +213,16 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createImportCustomEmojisJob(user: ThinUser, fileId: DriveFile['id']) {
 | 
				
			||||||
 | 
						return dbQueue.add('importCustomEmojis', {
 | 
				
			||||||
 | 
							user: user,
 | 
				
			||||||
 | 
							fileId: fileId,
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							removeOnComplete: true,
 | 
				
			||||||
 | 
							removeOnFail: true,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
 | 
					export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
 | 
				
			||||||
	return dbQueue.add('deleteAccount', {
 | 
						return dbQueue.add('deleteAccount', {
 | 
				
			||||||
		user: user,
 | 
							user: user,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await writeMeta(`{"metaVersion":1,"emojis":[`);
 | 
						await writeMeta(`{"metaVersion":2,"emojis":[`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const customEmojis = await Emojis.find({
 | 
						const customEmojis = await Emojis.find({
 | 
				
			||||||
		where: {
 | 
							where: {
 | 
				
			||||||
| 
						 | 
					@ -64,9 +64,9 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for (const emoji of customEmojis) {
 | 
						for (const emoji of customEmojis) {
 | 
				
			||||||
		const exportId = ulid().toLowerCase();
 | 
					 | 
				
			||||||
		const ext = mime.extension(emoji.type);
 | 
							const ext = mime.extension(emoji.type);
 | 
				
			||||||
		const emojiPath = path + '/' + exportId + (ext ? '.' + ext : '');
 | 
							const fileName = emoji.name + (ext ? '.' + ext : '');
 | 
				
			||||||
 | 
							const emojiPath = path + '/' + fileName;
 | 
				
			||||||
		fs.writeFileSync(emojiPath, '', 'binary');
 | 
							fs.writeFileSync(emojiPath, '', 'binary');
 | 
				
			||||||
		let downloaded = false;
 | 
							let downloaded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,8 +77,12 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 | 
				
			||||||
			logger.error(e);
 | 
								logger.error(e);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!downloaded) {
 | 
				
			||||||
 | 
								fs.unlinkSync(emojiPath);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const content = JSON.stringify({
 | 
							const content = JSON.stringify({
 | 
				
			||||||
			id: exportId,
 | 
								fileName: fileName,
 | 
				
			||||||
			downloaded: downloaded,
 | 
								downloaded: downloaded,
 | 
				
			||||||
			emoji: emoji,
 | 
								emoji: emoji,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					import * as Bull from 'bull';
 | 
				
			||||||
 | 
					import * as tmp from 'tmp';
 | 
				
			||||||
 | 
					import * as fs from 'fs';
 | 
				
			||||||
 | 
					const unzipper = require('unzipper');
 | 
				
			||||||
 | 
					import { getConnection } from 'typeorm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { queueLogger } from '../../logger';
 | 
				
			||||||
 | 
					import { downloadUrl } from '@/misc/download-url';
 | 
				
			||||||
 | 
					import { DriveFiles, Emojis } from '@/models/index';
 | 
				
			||||||
 | 
					import { DbUserImportJobData } from '@/queue/types';
 | 
				
			||||||
 | 
					import addFile from '@/services/drive/add-file';
 | 
				
			||||||
 | 
					import { genId } from '@/misc/gen-id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const logger = queueLogger.createSubLogger('import-custom-emojis');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: 名前衝突時の動作を選べるようにする
 | 
				
			||||||
 | 
					export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
 | 
				
			||||||
 | 
						logger.info(`Importing custom emojis ...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const file = await DriveFiles.findOne({
 | 
				
			||||||
 | 
							id: job.data.fileId,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (file == null) {
 | 
				
			||||||
 | 
							done();
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create temp dir
 | 
				
			||||||
 | 
						const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
 | 
				
			||||||
 | 
							tmp.dir((e, path, cleanup) => {
 | 
				
			||||||
 | 
								if (e) return rej(e);
 | 
				
			||||||
 | 
								res([path, cleanup]);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.info(`Temp dir is ${path}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const destPath = path + '/emojis.zip';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							fs.writeFileSync(destPath, '', 'binary');
 | 
				
			||||||
 | 
							await downloadUrl(file.url, destPath);
 | 
				
			||||||
 | 
						} catch (e) { // TODO: 何度か再試行
 | 
				
			||||||
 | 
							logger.error(e);
 | 
				
			||||||
 | 
							throw e;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const outputPath = path + '/emojis';
 | 
				
			||||||
 | 
						const unzipStream = fs.createReadStream(destPath);
 | 
				
			||||||
 | 
						const extractor = unzipper.Extract({ path: outputPath });
 | 
				
			||||||
 | 
						extractor.on('close', async () => {
 | 
				
			||||||
 | 
							const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
 | 
				
			||||||
 | 
							const meta = JSON.parse(metaRaw);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (const record of meta.emojis) {
 | 
				
			||||||
 | 
								if (!record.downloaded) continue;
 | 
				
			||||||
 | 
								const emojiInfo = record.emoji;
 | 
				
			||||||
 | 
								const emojiPath = outputPath + '/' + record.fileName;
 | 
				
			||||||
 | 
								await Emojis.delete({
 | 
				
			||||||
 | 
									name: emojiInfo.name,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								const driveFile = await addFile(null, emojiPath, record.fileName, null, null, true);
 | 
				
			||||||
 | 
								const emoji = await Emojis.insert({
 | 
				
			||||||
 | 
									id: genId(),
 | 
				
			||||||
 | 
									updatedAt: new Date(),
 | 
				
			||||||
 | 
									name: emojiInfo.name,
 | 
				
			||||||
 | 
									category: emojiInfo.category,
 | 
				
			||||||
 | 
									host: null,
 | 
				
			||||||
 | 
									aliases: emojiInfo.aliases,
 | 
				
			||||||
 | 
									url: driveFile.url,
 | 
				
			||||||
 | 
									type: driveFile.type,
 | 
				
			||||||
 | 
								}).then(x => Emojis.findOneOrFail(x.identifiers[0]));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							cleanup();
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							logger.succ('Imported');
 | 
				
			||||||
 | 
							done();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						unzipStream.pipe(extractor);
 | 
				
			||||||
 | 
						logger.succ(`Unzipping to ${outputPath}`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -12,6 +12,7 @@ import { importUserLists } from './import-user-lists';
 | 
				
			||||||
import { deleteAccount } from './delete-account';
 | 
					import { deleteAccount } from './delete-account';
 | 
				
			||||||
import { importMuting } from './import-muting';
 | 
					import { importMuting } from './import-muting';
 | 
				
			||||||
import { importBlocking } from './import-blocking';
 | 
					import { importBlocking } from './import-blocking';
 | 
				
			||||||
 | 
					import { importCustomEmojis } from './import-custom-emojis';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jobs = {
 | 
					const jobs = {
 | 
				
			||||||
	deleteDriveFiles,
 | 
						deleteDriveFiles,
 | 
				
			||||||
| 
						 | 
					@ -25,6 +26,7 @@ const jobs = {
 | 
				
			||||||
	importMuting,
 | 
						importMuting,
 | 
				
			||||||
	importBlocking,
 | 
						importBlocking,
 | 
				
			||||||
	importUserLists,
 | 
						importUserLists,
 | 
				
			||||||
 | 
						importCustomEmojis,
 | 
				
			||||||
	deleteAccount,
 | 
						deleteAccount,
 | 
				
			||||||
} as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
 | 
					} as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../../define';
 | 
				
			||||||
 | 
					import { ID } from '@/misc/cafy-id';
 | 
				
			||||||
 | 
					import { Emojis } from '@/models/index';
 | 
				
			||||||
 | 
					import { getConnection, In } from 'typeorm';
 | 
				
			||||||
 | 
					import { ApiError } from '../../../error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						tags: ['admin'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true as const,
 | 
				
			||||||
 | 
						requireModerator: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							ids: {
 | 
				
			||||||
 | 
								validator: $.arr($.type(ID)),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							aliases: {
 | 
				
			||||||
 | 
								validator: $.arr($.str),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default define(meta, async (ps) => {
 | 
				
			||||||
 | 
						const emojis = await Emojis.find({
 | 
				
			||||||
 | 
							id: In(ps.ids),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (const emoji of emojis) {
 | 
				
			||||||
 | 
							await Emojis.update(emoji.id, {
 | 
				
			||||||
 | 
								updatedAt: new Date(),
 | 
				
			||||||
 | 
								aliases: [...new Set(emoji.aliases.concat(ps.aliases))],
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../../define';
 | 
				
			||||||
 | 
					import { ID } from '@/misc/cafy-id';
 | 
				
			||||||
 | 
					import { Emojis } from '@/models/index';
 | 
				
			||||||
 | 
					import { getConnection, In } from 'typeorm';
 | 
				
			||||||
 | 
					import { insertModerationLog } from '@/services/insert-moderation-log';
 | 
				
			||||||
 | 
					import { ApiError } from '../../../error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						tags: ['admin'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true as const,
 | 
				
			||||||
 | 
						requireModerator: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							ids: {
 | 
				
			||||||
 | 
								validator: $.arr($.type(ID)),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default define(meta, async (ps, me) => {
 | 
				
			||||||
 | 
						const emojis = await Emojis.find({
 | 
				
			||||||
 | 
							id: In(ps.ids),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (const emoji of emojis) {
 | 
				
			||||||
 | 
							await Emojis.delete(emoji.id);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							insertModerationLog(me, 'deleteEmoji', {
 | 
				
			||||||
 | 
								emoji: emoji,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ export default define(meta, async (ps, me) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
						await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	insertModerationLog(me, 'removeEmoji', {
 | 
						insertModerationLog(me, 'deleteEmoji', {
 | 
				
			||||||
		emoji: emoji,
 | 
							emoji: emoji,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../../define';
 | 
				
			||||||
 | 
					import { createImportCustomEmojisJob } from '@/queue/index';
 | 
				
			||||||
 | 
					import ms from 'ms';
 | 
				
			||||||
 | 
					import { ID } from '@/misc/cafy-id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						secure: true,
 | 
				
			||||||
 | 
						requireCredential: true as const,
 | 
				
			||||||
 | 
						requireModerator: true,
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							fileId: {
 | 
				
			||||||
 | 
								validator: $.type(ID),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default define(meta, async (ps, user) => {
 | 
				
			||||||
 | 
						createImportCustomEmojisJob(user, ps.fileId);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../../define';
 | 
				
			||||||
 | 
					import { ID } from '@/misc/cafy-id';
 | 
				
			||||||
 | 
					import { Emojis } from '@/models/index';
 | 
				
			||||||
 | 
					import { getConnection, In } from 'typeorm';
 | 
				
			||||||
 | 
					import { ApiError } from '../../../error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						tags: ['admin'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true as const,
 | 
				
			||||||
 | 
						requireModerator: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							ids: {
 | 
				
			||||||
 | 
								validator: $.arr($.type(ID)),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							aliases: {
 | 
				
			||||||
 | 
								validator: $.arr($.str),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default define(meta, async (ps) => {
 | 
				
			||||||
 | 
						const emojis = await Emojis.find({
 | 
				
			||||||
 | 
							id: In(ps.ids),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (const emoji of emojis) {
 | 
				
			||||||
 | 
							await Emojis.update(emoji.id, {
 | 
				
			||||||
 | 
								updatedAt: new Date(),
 | 
				
			||||||
 | 
								aliases: emoji.aliases.filter(x => !ps.aliases.includes(x)),
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../../define';
 | 
				
			||||||
 | 
					import { ID } from '@/misc/cafy-id';
 | 
				
			||||||
 | 
					import { Emojis } from '@/models/index';
 | 
				
			||||||
 | 
					import { getConnection, In } from 'typeorm';
 | 
				
			||||||
 | 
					import { ApiError } from '../../../error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						tags: ['admin'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true as const,
 | 
				
			||||||
 | 
						requireModerator: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							ids: {
 | 
				
			||||||
 | 
								validator: $.arr($.type(ID)),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							aliases: {
 | 
				
			||||||
 | 
								validator: $.arr($.str),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default define(meta, async (ps) => {
 | 
				
			||||||
 | 
						await Emojis.update({
 | 
				
			||||||
 | 
							id: In(ps.ids),
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							updatedAt: new Date(),
 | 
				
			||||||
 | 
							aliases: ps.aliases,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import $ from 'cafy';
 | 
				
			||||||
 | 
					import define from '../../../define';
 | 
				
			||||||
 | 
					import { ID } from '@/misc/cafy-id';
 | 
				
			||||||
 | 
					import { Emojis } from '@/models/index';
 | 
				
			||||||
 | 
					import { getConnection, In } from 'typeorm';
 | 
				
			||||||
 | 
					import { ApiError } from '../../../error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const meta = {
 | 
				
			||||||
 | 
						tags: ['admin'],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requireCredential: true as const,
 | 
				
			||||||
 | 
						requireModerator: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						params: {
 | 
				
			||||||
 | 
							ids: {
 | 
				
			||||||
 | 
								validator: $.arr($.type(ID)),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							category: {
 | 
				
			||||||
 | 
								validator: $.optional.nullable.str,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default define(meta, async (ps) => {
 | 
				
			||||||
 | 
						await Emojis.update({
 | 
				
			||||||
 | 
							id: In(ps.ids),
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							updatedAt: new Date(),
 | 
				
			||||||
 | 
							category: ps.category,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await getConnection().queryResultCache!.remove(['meta_emojis']);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -23,7 +23,7 @@ import Logger from '@/services/logger';
 | 
				
			||||||
import { envOption } from '../env';
 | 
					import { envOption } from '../env';
 | 
				
			||||||
import { UserProfiles, Users } from '@/models/index';
 | 
					import { UserProfiles, Users } from '@/models/index';
 | 
				
			||||||
import { networkChart } from '@/services/chart/index';
 | 
					import { networkChart } from '@/services/chart/index';
 | 
				
			||||||
import { genAvatar } from '@/misc/gen-avatar';
 | 
					import { genIdenticon } from '@/misc/gen-identicon';
 | 
				
			||||||
import { createTemp } from '@/misc/create-temp';
 | 
					import { createTemp } from '@/misc/create-temp';
 | 
				
			||||||
import { publishMainStream } from '@/services/stream';
 | 
					import { publishMainStream } from '@/services/stream';
 | 
				
			||||||
import * as Acct from 'misskey-js/built/acct';
 | 
					import * as Acct from 'misskey-js/built/acct';
 | 
				
			||||||
| 
						 | 
					@ -84,9 +84,9 @@ router.get('/avatar/@:acct', async ctx => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/random-avatar/:x', async ctx => {
 | 
					router.get('/identicon/:x', async ctx => {
 | 
				
			||||||
	const [temp] = await createTemp();
 | 
						const [temp] = await createTemp();
 | 
				
			||||||
	await genAvatar(ctx.params.x, fs.createWriteStream(temp));
 | 
						await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
 | 
				
			||||||
	ctx.set('Content-Type', 'image/png');
 | 
						ctx.set('Content-Type', 'image/png');
 | 
				
			||||||
	ctx.body = fs.createReadStream(temp);
 | 
						ctx.body = fs.createReadStream(temp);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1522,6 +1522,11 @@ big-integer@^1.6.16:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
 | 
					  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e"
 | 
				
			||||||
  integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
 | 
					  integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					big-integer@^1.6.17:
 | 
				
			||||||
 | 
					  version "1.6.51"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
 | 
				
			||||||
 | 
					  integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
big.js@^5.2.2:
 | 
					big.js@^5.2.2:
 | 
				
			||||||
  version "5.2.2"
 | 
					  version "5.2.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
 | 
					  resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
 | 
				
			||||||
| 
						 | 
					@ -1532,6 +1537,14 @@ binary-extensions@^2.0.0:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
 | 
					  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
 | 
				
			||||||
  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
 | 
					  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					binary@~0.3.0:
 | 
				
			||||||
 | 
					  version "0.3.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79"
 | 
				
			||||||
 | 
					  integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    buffers "~0.1.1"
 | 
				
			||||||
 | 
					    chainsaw "~0.1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bl@^4.0.1, bl@^4.0.3:
 | 
					bl@^4.0.1, bl@^4.0.3:
 | 
				
			||||||
  version "4.0.3"
 | 
					  version "4.0.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
 | 
					  resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489"
 | 
				
			||||||
| 
						 | 
					@ -1546,6 +1559,11 @@ bluebird@^3.7.2:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
 | 
					  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
 | 
				
			||||||
  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 | 
					  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bluebird@~3.4.1:
 | 
				
			||||||
 | 
					  version "3.4.7"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
 | 
				
			||||||
 | 
					  integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
blurhash@1.1.4:
 | 
					blurhash@1.1.4:
 | 
				
			||||||
  version "1.1.4"
 | 
					  version "1.1.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
 | 
					  resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.4.tgz#a7010ceb3019cd2c9809b17c910ebf6175d29244"
 | 
				
			||||||
| 
						 | 
					@ -1677,6 +1695,11 @@ buffer-from@^1.0.0, buffer-from@^1.1.1:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
 | 
					  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
 | 
				
			||||||
  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 | 
					  integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					buffer-indexof-polyfill@~1.0.0:
 | 
				
			||||||
 | 
					  version "1.0.2"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz#d2732135c5999c64b277fcf9b1abe3498254729c"
 | 
				
			||||||
 | 
					  integrity sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
buffer-writer@2.0.0:
 | 
					buffer-writer@2.0.0:
 | 
				
			||||||
  version "2.0.0"
 | 
					  version "2.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
 | 
					  resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
 | 
				
			||||||
| 
						 | 
					@ -1707,6 +1730,11 @@ buffer@^6.0.3:
 | 
				
			||||||
    base64-js "^1.3.1"
 | 
					    base64-js "^1.3.1"
 | 
				
			||||||
    ieee754 "^1.2.1"
 | 
					    ieee754 "^1.2.1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					buffers@~0.1.1:
 | 
				
			||||||
 | 
					  version "0.1.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
 | 
				
			||||||
 | 
					  integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
bufferutil@^4.0.1:
 | 
					bufferutil@^4.0.1:
 | 
				
			||||||
  version "4.0.1"
 | 
					  version "4.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
 | 
					  resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.1.tgz#3a177e8e5819a1243fe16b63a199951a7ad8d4a7"
 | 
				
			||||||
| 
						 | 
					@ -1875,6 +1903,13 @@ cbor@8.1.0:
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    nofilter "^3.1.0"
 | 
					    nofilter "^3.1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chainsaw@~0.1.0:
 | 
				
			||||||
 | 
					  version "0.1.0"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98"
 | 
				
			||||||
 | 
					  integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    traverse ">=0.3.0 <0.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
chalk@4.0.0:
 | 
					chalk@4.0.0:
 | 
				
			||||||
  version "4.0.0"
 | 
					  version "4.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
 | 
					  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.0.0.tgz#6e98081ed2d17faab615eb52ac66ec1fe6209e72"
 | 
				
			||||||
| 
						 | 
					@ -2789,6 +2824,13 @@ dotenv@^8.2.0:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
 | 
					  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
 | 
				
			||||||
  integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
 | 
					  integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					duplexer2@~0.1.4:
 | 
				
			||||||
 | 
					  version "0.1.4"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
 | 
				
			||||||
 | 
					  integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    readable-stream "^2.0.2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ecc-jsbn@~0.1.1:
 | 
					ecc-jsbn@~0.1.1:
 | 
				
			||||||
  version "0.1.2"
 | 
					  version "0.1.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
 | 
					  resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
 | 
				
			||||||
| 
						 | 
					@ -3480,6 +3522,16 @@ fsevents@~2.1.2:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
 | 
					  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
 | 
				
			||||||
  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
 | 
					  integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fstream@^1.0.12:
 | 
				
			||||||
 | 
					  version "1.0.12"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
 | 
				
			||||||
 | 
					  integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    graceful-fs "^4.1.2"
 | 
				
			||||||
 | 
					    inherits "~2.0.0"
 | 
				
			||||||
 | 
					    mkdirp ">=0.5 0"
 | 
				
			||||||
 | 
					    rimraf "2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function-bind@^1.1.1:
 | 
					function-bind@^1.1.1:
 | 
				
			||||||
  version "1.1.1"
 | 
					  version "1.1.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 | 
					  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 | 
				
			||||||
| 
						 | 
					@ -3690,7 +3742,7 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.4:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
 | 
					  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
 | 
				
			||||||
  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
 | 
					  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
graceful-fs@^4.2.0:
 | 
					graceful-fs@^4.2.0, graceful-fs@^4.2.2:
 | 
				
			||||||
  version "4.2.8"
 | 
					  version "4.2.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
 | 
					  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
 | 
				
			||||||
  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 | 
					  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
 | 
				
			||||||
| 
						 | 
					@ -4007,7 +4059,7 @@ inflight@^1.0.4:
 | 
				
			||||||
    once "^1.3.0"
 | 
					    once "^1.3.0"
 | 
				
			||||||
    wrappy "1"
 | 
					    wrappy "1"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
 | 
					inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
 | 
				
			||||||
  version "2.0.4"
 | 
					  version "2.0.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
 | 
					  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
 | 
				
			||||||
  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 | 
					  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
 | 
				
			||||||
| 
						 | 
					@ -4800,6 +4852,11 @@ lilconfig@^2.0.3:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
 | 
					  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
 | 
				
			||||||
  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
 | 
					  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					listenercount@~1.0.1:
 | 
				
			||||||
 | 
					  version "1.0.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937"
 | 
				
			||||||
 | 
					  integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
loader-runner@^4.2.0:
 | 
					loader-runner@^4.2.0:
 | 
				
			||||||
  version "4.2.0"
 | 
					  version "4.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
 | 
					  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
 | 
				
			||||||
| 
						 | 
					@ -5204,7 +5261,7 @@ mkdirp-classic@^0.5.3:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
 | 
					  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
 | 
				
			||||||
  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
 | 
					  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mkdirp@0.x, mkdirp@^0.5.4:
 | 
					mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.4:
 | 
				
			||||||
  version "0.5.5"
 | 
					  version "0.5.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
 | 
					  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
 | 
				
			||||||
  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
 | 
					  integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
 | 
				
			||||||
| 
						 | 
					@ -6598,7 +6655,7 @@ readable-stream@1.1.x:
 | 
				
			||||||
    isarray "0.0.1"
 | 
					    isarray "0.0.1"
 | 
				
			||||||
    string_decoder "~0.10.x"
 | 
					    string_decoder "~0.10.x"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2:
 | 
					readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6:
 | 
				
			||||||
  version "2.3.7"
 | 
					  version "2.3.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
 | 
					  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
 | 
				
			||||||
  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
 | 
					  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
 | 
				
			||||||
| 
						 | 
					@ -6780,6 +6837,13 @@ reusify@^1.0.4:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
 | 
					  resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
 | 
				
			||||||
  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 | 
					  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rimraf@2:
 | 
				
			||||||
 | 
					  version "2.7.1"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
 | 
				
			||||||
 | 
					  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    glob "^7.1.3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
 | 
					rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
 | 
				
			||||||
  version "3.0.2"
 | 
					  version "3.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
 | 
					  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
 | 
				
			||||||
| 
						 | 
					@ -6914,7 +6978,7 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
 | 
					  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
 | 
				
			||||||
  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
 | 
					  integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
setimmediate@^1.0.5:
 | 
					setimmediate@^1.0.5, setimmediate@~1.0.4:
 | 
				
			||||||
  version "1.0.5"
 | 
					  version "1.0.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
 | 
					  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
 | 
				
			||||||
  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
 | 
					  integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
 | 
				
			||||||
| 
						 | 
					@ -7584,6 +7648,11 @@ trace-redirect@1.0.6:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/trace-redirect/-/trace-redirect-1.0.6.tgz#ac629b5bf8247d30dde5a35fe9811b811075b504"
 | 
					  resolved "https://registry.yarnpkg.com/trace-redirect/-/trace-redirect-1.0.6.tgz#ac629b5bf8247d30dde5a35fe9811b811075b504"
 | 
				
			||||||
  integrity sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==
 | 
					  integrity sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"traverse@>=0.3.0 <0.4":
 | 
				
			||||||
 | 
					  version "0.3.9"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
 | 
				
			||||||
 | 
					  integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ts-jest@^25.2.1:
 | 
					ts-jest@^25.2.1:
 | 
				
			||||||
  version "25.5.1"
 | 
					  version "25.5.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"
 | 
					  resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7"
 | 
				
			||||||
| 
						 | 
					@ -7827,6 +7896,22 @@ unpipe@1.0.0:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 | 
					  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
 | 
				
			||||||
  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
 | 
					  integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					unzipper@0.10.11:
 | 
				
			||||||
 | 
					  version "0.10.11"
 | 
				
			||||||
 | 
					  resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.11.tgz#0b4991446472cbdb92ee7403909f26c2419c782e"
 | 
				
			||||||
 | 
					  integrity sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==
 | 
				
			||||||
 | 
					  dependencies:
 | 
				
			||||||
 | 
					    big-integer "^1.6.17"
 | 
				
			||||||
 | 
					    binary "~0.3.0"
 | 
				
			||||||
 | 
					    bluebird "~3.4.1"
 | 
				
			||||||
 | 
					    buffer-indexof-polyfill "~1.0.0"
 | 
				
			||||||
 | 
					    duplexer2 "~0.1.4"
 | 
				
			||||||
 | 
					    fstream "^1.0.12"
 | 
				
			||||||
 | 
					    graceful-fs "^4.2.2"
 | 
				
			||||||
 | 
					    listenercount "~1.0.1"
 | 
				
			||||||
 | 
					    readable-stream "~2.3.6"
 | 
				
			||||||
 | 
					    setimmediate "~1.0.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
uri-js@^4.2.2:
 | 
					uri-js@^4.2.2:
 | 
				
			||||||
  version "4.2.2"
 | 
					  version "4.2.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
 | 
					  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,10 @@ module.exports = {
 | 
				
			||||||
		"plugin:vue/vue3-recommended"
 | 
							"plugin:vue/vue3-recommended"
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
	rules: {
 | 
						rules: {
 | 
				
			||||||
 | 
							// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
 | 
				
			||||||
 | 
							// data の禁止理由: 抽象的すぎるため
 | 
				
			||||||
 | 
							// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | 
				
			||||||
 | 
							"id-denylist": ["error", "window", "data", "e"],
 | 
				
			||||||
		"vue/attributes-order": ["error", {
 | 
							"vue/attributes-order": ["error", {
 | 
				
			||||||
			"alphabetical": false
 | 
								"alphabetical": false
 | 
				
			||||||
		}],
 | 
							}],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,6 @@
 | 
				
			||||||
		"@types/katex": "0.11.1",
 | 
							"@types/katex": "0.11.1",
 | 
				
			||||||
		"@types/matter-js": "0.17.6",
 | 
							"@types/matter-js": "0.17.6",
 | 
				
			||||||
		"@types/mocha": "8.2.3",
 | 
							"@types/mocha": "8.2.3",
 | 
				
			||||||
		"@types/node": "16.11.12",
 | 
					 | 
				
			||||||
		"@types/oauth": "0.9.1",
 | 
							"@types/oauth": "0.9.1",
 | 
				
			||||||
		"@types/parse5": "6.0.3",
 | 
							"@types/parse5": "6.0.3",
 | 
				
			||||||
		"@types/punycode": "2.1.0",
 | 
							"@types/punycode": "2.1.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,13 +10,13 @@
 | 
				
			||||||
					<XCwButton v-model="showContent" :note="note"/>
 | 
										<XCwButton v-model="showContent" :note="note"/>
 | 
				
			||||||
				</p>
 | 
									</p>
 | 
				
			||||||
				<div v-show="note.cw == null || showContent" class="content">
 | 
									<div v-show="note.cw == null || showContent" class="content">
 | 
				
			||||||
					<XSubNote-content class="text" :note="note"/>
 | 
										<MkNoteSubNoteContent class="text" :note="note"/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<template v-if="depth < 5">
 | 
						<template v-if="depth < 5">
 | 
				
			||||||
		<XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
 | 
							<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
	<div v-else class="more">
 | 
						<div v-else class="more">
 | 
				
			||||||
		<MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA>
 | 
							<MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA>
 | 
				
			||||||
| 
						 | 
					@ -24,63 +24,36 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import { notePage } from '@/filters/note';
 | 
					import { notePage } from '@/filters/note';
 | 
				
			||||||
import XNoteHeader from './note-header.vue';
 | 
					import XNoteHeader from './note-header.vue';
 | 
				
			||||||
import XSubNoteContent from './sub-note-content.vue';
 | 
					import MkNoteSubNoteContent from './sub-note-content.vue';
 | 
				
			||||||
import XCwButton from './cw-button.vue';
 | 
					import XCwButton from './cw-button.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	name: 'XSub',
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
 | 
						detail?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		XNoteHeader,
 | 
					 | 
				
			||||||
		XSubNoteContent,
 | 
					 | 
				
			||||||
		XCwButton,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		note: {
 | 
					 | 
				
			||||||
			type: Object,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		detail: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	// how many notes are in between this one and the note being viewed in detail
 | 
						// how many notes are in between this one and the note being viewed in detail
 | 
				
			||||||
		depth: {
 | 
						depth?: number;
 | 
				
			||||||
			type: Number,
 | 
					}>(), {
 | 
				
			||||||
			required: false,
 | 
						depth: 1,
 | 
				
			||||||
			default: 1
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			showContent: false,
 | 
					 | 
				
			||||||
			replies: [],
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		if (this.detail) {
 | 
					 | 
				
			||||||
			os.api('notes/children', {
 | 
					 | 
				
			||||||
				noteId: this.note.id,
 | 
					 | 
				
			||||||
				limit: 5
 | 
					 | 
				
			||||||
			}).then(replies => {
 | 
					 | 
				
			||||||
				this.replies = replies;
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		notePage,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let showContent = $ref(false);
 | 
				
			||||||
 | 
					let replies: misskey.entities.Note[] = $ref([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (props.detail) {
 | 
				
			||||||
 | 
						os.api('notes/children', {
 | 
				
			||||||
 | 
							noteId: props.note.id,
 | 
				
			||||||
 | 
							limit: 5
 | 
				
			||||||
 | 
						}).then(res => {
 | 
				
			||||||
 | 
							replies = res;
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,7 @@ onMounted(() => {
 | 
				
			||||||
	const update = () => {
 | 
						const update = () => {
 | 
				
			||||||
		if (enabled.value) {
 | 
							if (enabled.value) {
 | 
				
			||||||
			tick();
 | 
								tick();
 | 
				
			||||||
			setTimeout(update, 1000);
 | 
								window.setTimeout(update, 1000);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	update();
 | 
						update();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,7 @@ function requestRender() {
 | 
				
			||||||
			'error-callback': callback,
 | 
								'error-callback': callback,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		setTimeout(requestRender, 1);
 | 
							window.setTimeout(requestRender, 1);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -167,7 +167,7 @@ export default defineComponent({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// このコンポーネントが作成された時、非表示状態である場合がある
 | 
									// このコンポーネントが作成された時、非表示状態である場合がある
 | 
				
			||||||
				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
 | 
									// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
 | 
				
			||||||
				const clock = setInterval(() => {
 | 
									const clock = window.setInterval(() => {
 | 
				
			||||||
					if (prefixEl.value) {
 | 
										if (prefixEl.value) {
 | 
				
			||||||
						if (prefixEl.value.offsetWidth) {
 | 
											if (prefixEl.value.offsetWidth) {
 | 
				
			||||||
							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
 | 
												inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
 | 
				
			||||||
| 
						 | 
					@ -181,7 +181,7 @@ export default defineComponent({
 | 
				
			||||||
				}, 100);
 | 
									}, 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				onUnmounted(() => {
 | 
									onUnmounted(() => {
 | 
				
			||||||
					clearInterval(clock);
 | 
										window.clearInterval(clock);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,7 +117,7 @@ export default defineComponent({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// このコンポーネントが作成された時、非表示状態である場合がある
 | 
									// このコンポーネントが作成された時、非表示状態である場合がある
 | 
				
			||||||
				// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
 | 
									// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
 | 
				
			||||||
				const clock = setInterval(() => {
 | 
									const clock = window.setInterval(() => {
 | 
				
			||||||
					if (prefixEl.value) {
 | 
										if (prefixEl.value) {
 | 
				
			||||||
						if (prefixEl.value.offsetWidth) {
 | 
											if (prefixEl.value.offsetWidth) {
 | 
				
			||||||
							inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
 | 
												inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
 | 
				
			||||||
| 
						 | 
					@ -131,7 +131,7 @@ export default defineComponent({
 | 
				
			||||||
				}, 100);
 | 
									}, 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				onUnmounted(() => {
 | 
									onUnmounted(() => {
 | 
				
			||||||
					clearInterval(clock);
 | 
										window.clearInterval(clock);
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,130 +4,114 @@
 | 
				
			||||||
</a>
 | 
					</a>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { inject } from 'vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
				
			||||||
import { router } from '@/router';
 | 
					import { router } from '@/router';
 | 
				
			||||||
import { url } from '@/config';
 | 
					import { url } from '@/config';
 | 
				
			||||||
import { popout } from '@/scripts/popout';
 | 
					import { popout as popout_ } from '@/scripts/popout';
 | 
				
			||||||
import { ColdDeviceStorage } from '@/store';
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					import { defaultStore } from '@/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	inject: {
 | 
						to: string;
 | 
				
			||||||
		navHook: {
 | 
						activeClass?: null | string;
 | 
				
			||||||
			default: null
 | 
						behavior?: null | 'window' | 'browser' | 'modalWindow';
 | 
				
			||||||
		},
 | 
					}>(), {
 | 
				
			||||||
		sideViewHook: {
 | 
						activeClass: null,
 | 
				
			||||||
			default: null
 | 
						behavior: null,
 | 
				
			||||||
		}
 | 
					});
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
					const navHook = inject('navHook', null);
 | 
				
			||||||
		to: {
 | 
					const sideViewHook = inject('sideViewHook', null);
 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		activeClass: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		behavior: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
					const active = $computed(() => {
 | 
				
			||||||
		active() {
 | 
						if (props.activeClass == null) return false;
 | 
				
			||||||
			if (this.activeClass == null) return false;
 | 
						const resolved = router.resolve(props.to);
 | 
				
			||||||
			const resolved = router.resolve(this.to);
 | 
						if (resolved.path === router.currentRoute.value.path) return true;
 | 
				
			||||||
			if (resolved.path == this.$route.path) return true;
 | 
					 | 
				
			||||||
	if (resolved.name == null) return false;
 | 
						if (resolved.name == null) return false;
 | 
				
			||||||
			if (this.$route.name == null) return false;
 | 
						if (router.currentRoute.value.name == null) return false;
 | 
				
			||||||
			return resolved.name == this.$route.name;
 | 
						return resolved.name === router.currentRoute.value.name;
 | 
				
			||||||
		}
 | 
					});
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	methods: {
 | 
					function onContextmenu(ev) {
 | 
				
			||||||
		onContextmenu(e) {
 | 
						const selection = window.getSelection();
 | 
				
			||||||
			if (window.getSelection().toString() !== '') return;
 | 
						if (selection && selection.toString() !== '') return;
 | 
				
			||||||
	os.contextMenu([{
 | 
						os.contextMenu([{
 | 
				
			||||||
		type: 'label',
 | 
							type: 'label',
 | 
				
			||||||
				text: this.to,
 | 
							text: props.to,
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
		icon: 'fas fa-window-maximize',
 | 
							icon: 'fas fa-window-maximize',
 | 
				
			||||||
				text: this.$ts.openInWindow,
 | 
							text: i18n.locale.openInWindow,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
					os.pageWindow(this.to);
 | 
								os.pageWindow(props.to);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			}, this.sideViewHook ? {
 | 
						}, sideViewHook ? {
 | 
				
			||||||
		icon: 'fas fa-columns',
 | 
							icon: 'fas fa-columns',
 | 
				
			||||||
				text: this.$ts.openInSideView,
 | 
							text: i18n.locale.openInSideView,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
					this.sideViewHook(this.to);
 | 
								sideViewHook(props.to);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} : undefined, {
 | 
						} : undefined, {
 | 
				
			||||||
		icon: 'fas fa-expand-alt',
 | 
							icon: 'fas fa-expand-alt',
 | 
				
			||||||
				text: this.$ts.showInPage,
 | 
							text: i18n.locale.showInPage,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
					this.$router.push(this.to);
 | 
								router.push(props.to);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}, null, {
 | 
						}, null, {
 | 
				
			||||||
		icon: 'fas fa-external-link-alt',
 | 
							icon: 'fas fa-external-link-alt',
 | 
				
			||||||
				text: this.$ts.openInNewTab,
 | 
							text: i18n.locale.openInNewTab,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
					window.open(this.to, '_blank');
 | 
								window.open(props.to, '_blank');
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
		icon: 'fas fa-link',
 | 
							icon: 'fas fa-link',
 | 
				
			||||||
				text: this.$ts.copyLink,
 | 
							text: i18n.locale.copyLink,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
					copyToClipboard(`${url}${this.to}`);
 | 
								copyToClipboard(`${url}${props.to}`);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			}], e);
 | 
						}], ev);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		window() {
 | 
					function openWindow() {
 | 
				
			||||||
			os.pageWindow(this.to);
 | 
						os.pageWindow(props.to);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		modalWindow() {
 | 
					function modalWindow() {
 | 
				
			||||||
			os.modalPageWindow(this.to);
 | 
						os.modalPageWindow(props.to);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		popout() {
 | 
					function popout() {
 | 
				
			||||||
			popout(this.to);
 | 
						popout_(props.to);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		nav() {
 | 
					function nav() {
 | 
				
			||||||
			if (this.behavior === 'browser') {
 | 
						if (props.behavior === 'browser') {
 | 
				
			||||||
				location.href = this.to;
 | 
							location.href = props.to;
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.behavior) {
 | 
						if (props.behavior) {
 | 
				
			||||||
				if (this.behavior === 'window') {
 | 
							if (props.behavior === 'window') {
 | 
				
			||||||
					return this.window();
 | 
								return openWindow();
 | 
				
			||||||
				} else if (this.behavior === 'modalWindow') {
 | 
							} else if (props.behavior === 'modalWindow') {
 | 
				
			||||||
					return this.modalWindow();
 | 
								return modalWindow();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.navHook) {
 | 
						if (navHook) {
 | 
				
			||||||
				this.navHook(this.to);
 | 
							navHook(props.to);
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
				if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
 | 
							if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') {
 | 
				
			||||||
					return this.sideViewHook(this.to);
 | 
								return sideViewHook(props.to);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (this.$router.currentRoute.value.path === this.to) {
 | 
							if (router.currentRoute.value.path === props.to) {
 | 
				
			||||||
			window.scroll({ top: 0, behavior: 'smooth' });
 | 
								window.scroll({ top: 0, behavior: 'smooth' });
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
					this.$router.push(this.to);
 | 
								router.push(props.to);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		}
 | 
					}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
import { defineComponent, ref } from 'vue';
 | 
					import { defineComponent, ref } from 'vue';
 | 
				
			||||||
import { Instance, instance } from '@/instance';
 | 
					import { instance } from '@/instance';
 | 
				
			||||||
import { host } from '@/config';
 | 
					import { host } from '@/config';
 | 
				
			||||||
import MkButton from '@/components/ui/button.vue';
 | 
					import MkButton from '@/components/ui/button.vue';
 | 
				
			||||||
import { defaultStore } from '@/store';
 | 
					import { defaultStore } from '@/store';
 | 
				
			||||||
| 
						 | 
					@ -48,9 +48,9 @@ export default defineComponent({
 | 
				
			||||||
			showMenu.value = !showMenu.value;
 | 
								showMenu.value = !showMenu.value;
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const choseAd = (): Instance['ads'][number] | null => {
 | 
							const choseAd = (): (typeof instance)['ads'][number] | null => {
 | 
				
			||||||
			if (props.specify) {
 | 
								if (props.specify) {
 | 
				
			||||||
				return props.specify as Instance['ads'][number];
 | 
									return props.specify as (typeof instance)['ads'][number];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
 | 
								const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,74 +1,54 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" @click="onClick">
 | 
					<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick">
 | 
				
			||||||
	<img class="inner" :src="url" decoding="async"/>
 | 
						<img class="inner" :src="url" decoding="async"/>
 | 
				
			||||||
	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 | 
						<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 | 
				
			||||||
</span>
 | 
					</span>
 | 
				
			||||||
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target">
 | 
					<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
 | 
				
			||||||
	<img class="inner" :src="url" decoding="async"/>
 | 
						<img class="inner" :src="url" decoding="async"/>
 | 
				
			||||||
	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 | 
						<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 | 
				
			||||||
</MkA>
 | 
					</MkA>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { onMounted, watch } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 | 
					import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 | 
				
			||||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
 | 
					import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
 | 
				
			||||||
import { acct, userPage } from '@/filters/user';
 | 
					import { acct, userPage } from '@/filters/user';
 | 
				
			||||||
import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
 | 
					import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
 | 
				
			||||||
 | 
					import { defaultStore } from '@/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	components: {
 | 
						user: misskey.entities.User;
 | 
				
			||||||
		MkUserOnlineIndicator
 | 
						target?: string | null;
 | 
				
			||||||
	},
 | 
						disableLink?: boolean;
 | 
				
			||||||
	props: {
 | 
						disablePreview?: boolean;
 | 
				
			||||||
		user: {
 | 
						showIndicator?: boolean;
 | 
				
			||||||
			type: Object,
 | 
					}>(), {
 | 
				
			||||||
			required: true
 | 
						target: null,
 | 
				
			||||||
		},
 | 
						disableLink: false,
 | 
				
			||||||
		target: {
 | 
						disablePreview: false,
 | 
				
			||||||
			required: false,
 | 
						showIndicator: false,
 | 
				
			||||||
			default: null
 | 
					});
 | 
				
			||||||
		},
 | 
					
 | 
				
			||||||
		disableLink: {
 | 
					const emit = defineEmits<{
 | 
				
			||||||
			required: false,
 | 
						(e: 'click', ev: MouseEvent): void;
 | 
				
			||||||
			default: false
 | 
					}>();
 | 
				
			||||||
		},
 | 
					
 | 
				
			||||||
		disablePreview: {
 | 
					const url = defaultStore.state.disableShowingAnimatedImages
 | 
				
			||||||
			required: false,
 | 
						? getStaticImageUrl(props.user.avatarUrl)
 | 
				
			||||||
			default: false
 | 
						: props.user.avatarUrl;
 | 
				
			||||||
		},
 | 
					
 | 
				
			||||||
		showIndicator: {
 | 
					function onClick(ev: MouseEvent) {
 | 
				
			||||||
			required: false,
 | 
						emit('click', ev);
 | 
				
			||||||
			default: false
 | 
					}
 | 
				
			||||||
		}
 | 
					
 | 
				
			||||||
	},
 | 
					let color = $ref();
 | 
				
			||||||
	emits: ['click'],
 | 
					
 | 
				
			||||||
	computed: {
 | 
					watch(() => props.user.avatarBlurhash, () => {
 | 
				
			||||||
		cat(): boolean {
 | 
						color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
 | 
				
			||||||
			return this.user.isCat;
 | 
					}, {
 | 
				
			||||||
		},
 | 
						immediate: true,
 | 
				
			||||||
		url(): string {
 | 
					 | 
				
			||||||
			return this.$store.state.disableShowingAnimatedImages
 | 
					 | 
				
			||||||
				? getStaticImageUrl(this.user.avatarUrl)
 | 
					 | 
				
			||||||
				: this.user.avatarUrl;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	watch: {
 | 
					 | 
				
			||||||
		'user.avatarBlurhash'() {
 | 
					 | 
				
			||||||
			if (this.$el == null) return;
 | 
					 | 
				
			||||||
			this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		onClick(e) {
 | 
					 | 
				
			||||||
			this.$emit('click', e);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		acct,
 | 
					 | 
				
			||||||
		userPage
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,27 +4,17 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						inline?: boolean;
 | 
				
			||||||
		inline: {
 | 
						colored?: boolean;
 | 
				
			||||||
			type: Boolean,
 | 
						mini?: boolean;
 | 
				
			||||||
			required: false,
 | 
					}>(), {
 | 
				
			||||||
			default: false
 | 
						inline: false,
 | 
				
			||||||
		},
 | 
						colored: true,
 | 
				
			||||||
		colored: {
 | 
						mini: false,
 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		mini: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,23 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
 | 
					<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :customEmojis="customEmojis" :isNote="isNote" class="havbbuyv" :class="{ nowrap }"/>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import MfmCore from '@/components/mfm';
 | 
					import MfmCore from '@/components/mfm';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	components: {
 | 
						text: string;
 | 
				
			||||||
		MfmCore
 | 
						plain?: boolean;
 | 
				
			||||||
	}
 | 
						nowrap?: boolean;
 | 
				
			||||||
 | 
						author?: any;
 | 
				
			||||||
 | 
						customEmojis?: any;
 | 
				
			||||||
 | 
						isNote?: boolean;
 | 
				
			||||||
 | 
					}>(), {
 | 
				
			||||||
 | 
						plain: false,
 | 
				
			||||||
 | 
						nowrap: false,
 | 
				
			||||||
 | 
						author: null,
 | 
				
			||||||
 | 
						isNote: true,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@ export default defineComponent({
 | 
				
			||||||
			calc();
 | 
								calc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const observer = new MutationObserver(() => {
 | 
								const observer = new MutationObserver(() => {
 | 
				
			||||||
				setTimeout(() => {
 | 
									window.setTimeout(() => {
 | 
				
			||||||
					calc();
 | 
										calc();
 | 
				
			||||||
				}, 100);
 | 
									}, 100);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,73 +1,57 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<time :title="absolute">
 | 
					<time :title="absolute">
 | 
				
			||||||
	<template v-if="mode == 'relative'">{{ relative }}</template>
 | 
						<template v-if="mode === 'relative'">{{ relative }}</template>
 | 
				
			||||||
	<template v-else-if="mode == 'absolute'">{{ absolute }}</template>
 | 
						<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
 | 
				
			||||||
	<template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>
 | 
						<template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
 | 
				
			||||||
</time>
 | 
					</time>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { onUnmounted } from 'vue';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						time: Date | string;
 | 
				
			||||||
		time: {
 | 
						mode?: 'relative' | 'absolute' | 'detail';
 | 
				
			||||||
			type: [Date, String],
 | 
					}>(), {
 | 
				
			||||||
			required: true
 | 
						mode: 'relative',
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		mode: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			default: 'relative'
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			tickId: null,
 | 
					 | 
				
			||||||
			now: new Date()
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	computed: {
 | 
					 | 
				
			||||||
		_time(): Date {
 | 
					 | 
				
			||||||
			return typeof this.time == 'string' ? new Date(this.time) : this.time;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		absolute(): string {
 | 
					 | 
				
			||||||
			return this._time.toLocaleString();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		relative(): string {
 | 
					 | 
				
			||||||
			const time = this._time;
 | 
					 | 
				
			||||||
			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
 | 
					 | 
				
			||||||
			return (
 | 
					 | 
				
			||||||
				ago >= 31536000 ? this.$t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= 2592000  ? this.$t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= 604800   ? this.$t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= 86400    ? this.$t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= 3600     ? this.$t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= 60       ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= 10       ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
 | 
					 | 
				
			||||||
				ago >= -1       ? this.$ts._ago.justNow :
 | 
					 | 
				
			||||||
				ago <  -1       ? this.$ts._ago.future :
 | 
					 | 
				
			||||||
				this.$ts._ago.unknown);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		if (this.mode == 'relative' || this.mode == 'detail') {
 | 
					 | 
				
			||||||
			this.tickId = window.requestAnimationFrame(this.tick);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	unmounted() {
 | 
					 | 
				
			||||||
		if (this.mode === 'relative' || this.mode === 'detail') {
 | 
					 | 
				
			||||||
			window.clearTimeout(this.tickId);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		tick() {
 | 
					 | 
				
			||||||
			// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
 | 
					 | 
				
			||||||
			this.now = new Date();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.tickId = setTimeout(() => {
 | 
					 | 
				
			||||||
				window.requestAnimationFrame(this.tick);
 | 
					 | 
				
			||||||
			}, 10000);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const _time = typeof props.time == 'string' ? new Date(props.time) : props.time;
 | 
				
			||||||
 | 
					const absolute = _time.toLocaleString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let now = $ref(new Date());
 | 
				
			||||||
 | 
					const relative = $computed(() => {
 | 
				
			||||||
 | 
						const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
 | 
				
			||||||
 | 
						return (
 | 
				
			||||||
 | 
							ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
 | 
				
			||||||
 | 
							ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
 | 
				
			||||||
 | 
							ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
 | 
				
			||||||
 | 
							ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
 | 
				
			||||||
 | 
							ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
 | 
				
			||||||
 | 
							ago >= 60       ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
 | 
				
			||||||
 | 
							ago >= 10       ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
 | 
				
			||||||
 | 
							ago >= -1       ? i18n.locale._ago.justNow :
 | 
				
			||||||
 | 
							ago <  -1       ? i18n.locale._ago.future :
 | 
				
			||||||
 | 
							i18n.locale._ago.unknown);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function tick() {
 | 
				
			||||||
 | 
						// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
 | 
				
			||||||
 | 
						now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tickId = window.setTimeout(() => {
 | 
				
			||||||
 | 
							window.requestAnimationFrame(tick);
 | 
				
			||||||
 | 
						}, 10000);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let tickId: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (props.mode === 'relative' || props.mode === 'detail') {
 | 
				
			||||||
 | 
						tickId = window.requestAnimationFrame(tick);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onUnmounted(() => {
 | 
				
			||||||
 | 
							window.clearTimeout(tickId);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,19 +2,14 @@
 | 
				
			||||||
<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
 | 
					<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						user: misskey.entities.User;
 | 
				
			||||||
		user: {
 | 
						nowrap?: boolean;
 | 
				
			||||||
			type: Object,
 | 
					}>(), {
 | 
				
			||||||
			required: true
 | 
						nowrap: true,
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		nowrap: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			default: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
 | 
					<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
 | 
				
			||||||
	<div class="xubzgfga">
 | 
						<div class="xubzgfga">
 | 
				
			||||||
		<header>{{ image.name }}</header>
 | 
							<header>{{ image.name }}</header>
 | 
				
			||||||
		<img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
 | 
							<img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
 | 
				
			||||||
		<footer>
 | 
							<footer>
 | 
				
			||||||
			<span>{{ image.type }}</span>
 | 
								<span>{{ image.type }}</span>
 | 
				
			||||||
			<span>{{ bytes(image.size) }}</span>
 | 
								<span>{{ bytes(image.size) }}</span>
 | 
				
			||||||
| 
						 | 
					@ -12,31 +12,23 @@
 | 
				
			||||||
</MkModal>
 | 
					</MkModal>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import bytes from '@/filters/bytes';
 | 
					import bytes from '@/filters/bytes';
 | 
				
			||||||
import number from '@/filters/number';
 | 
					import number from '@/filters/number';
 | 
				
			||||||
import MkModal from '@/components/ui/modal.vue';
 | 
					import MkModal from '@/components/ui/modal.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	components: {
 | 
						image: misskey.entities.DriveFile;
 | 
				
			||||||
		MkModal,
 | 
					}>(), {
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		image: {
 | 
					 | 
				
			||||||
			type: Object,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	emits: ['closed'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		bytes,
 | 
					 | 
				
			||||||
		number,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'closed'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const modal = $ref<InstanceType<typeof MkModal>>();
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,67 +5,43 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { onMounted } from 'vue';
 | 
				
			||||||
import { decode } from 'blurhash';
 | 
					import { decode } from 'blurhash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						src?: string | null;
 | 
				
			||||||
		src: {
 | 
						hash: string;
 | 
				
			||||||
			type: String,
 | 
						alt?: string;
 | 
				
			||||||
			required: false,
 | 
						title?: string | null;
 | 
				
			||||||
			default: null
 | 
						size?: number;
 | 
				
			||||||
		},
 | 
						cover?: boolean;
 | 
				
			||||||
		hash: {
 | 
					}>(), {
 | 
				
			||||||
			type: String,
 | 
						src: null,
 | 
				
			||||||
			required: true
 | 
						alt: '',
 | 
				
			||||||
		},
 | 
						title: null,
 | 
				
			||||||
		alt: {
 | 
						size: 64,
 | 
				
			||||||
			type: String,
 | 
						cover: true,
 | 
				
			||||||
			required: false,
 | 
					});
 | 
				
			||||||
			default: '',
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		title: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: null,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		size: {
 | 
					 | 
				
			||||||
			type: Number,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: 64
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		cover: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					const canvas = $ref<HTMLCanvasElement>();
 | 
				
			||||||
		return {
 | 
					let loaded = $ref(false);
 | 
				
			||||||
			loaded: false,
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					function draw() {
 | 
				
			||||||
		this.draw();
 | 
						if (props.hash == null) return;
 | 
				
			||||||
	},
 | 
						const pixels = decode(props.hash, props.size, props.size);
 | 
				
			||||||
 | 
						const ctx = canvas.getContext('2d');
 | 
				
			||||||
	methods: {
 | 
						const imageData = ctx!.createImageData(props.size, props.size);
 | 
				
			||||||
		draw() {
 | 
					 | 
				
			||||||
			if (this.hash == null) return;
 | 
					 | 
				
			||||||
			const pixels = decode(this.hash, this.size, this.size);
 | 
					 | 
				
			||||||
			const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
 | 
					 | 
				
			||||||
			const imageData = ctx!.createImageData(this.size, this.size);
 | 
					 | 
				
			||||||
	imageData.data.set(pixels);
 | 
						imageData.data.set(pixels);
 | 
				
			||||||
	ctx!.putImageData(imageData, 0, 0);
 | 
						ctx!.putImageData(imageData, 0, 0);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		onLoad() {
 | 
					function onLoad() {
 | 
				
			||||||
			this.loaded = true;
 | 
						loaded = true;
 | 
				
			||||||
		}
 | 
					}
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
						draw();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,41 +1,22 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="hpaizdrt" :style="bg">
 | 
					<div class="hpaizdrt" :style="bg">
 | 
				
			||||||
	<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
 | 
						<img v-if="instance.faviconUrl" class="icon" :src="instance.faviconUrl"/>
 | 
				
			||||||
	<span class="name">{{ info.name }}</span>
 | 
						<span class="name">{{ instance.name }}</span>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import { instanceName } from '@/config';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	props: {
 | 
						instance: any; // TODO
 | 
				
			||||||
		instance: {
 | 
					}>();
 | 
				
			||||||
			type: Object,
 | 
					 | 
				
			||||||
			required: false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					const themeColor = props.instance.themeColor || '#777777';
 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			info: this.instance || {
 | 
					 | 
				
			||||||
				faviconUrl: '/favicon.ico',
 | 
					 | 
				
			||||||
				name: instanceName,
 | 
					 | 
				
			||||||
				themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
					const bg = {
 | 
				
			||||||
		bg(): any {
 | 
					 | 
				
			||||||
			const themeColor = this.info.themeColor || '#777777';
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
	background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
 | 
						background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
 | 
				
			||||||
			};
 | 
					};
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,82 +1,36 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
 | 
					<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
 | 
				
			||||||
	:title="url"
 | 
						:title="url"
 | 
				
			||||||
	@mouseover="onMouseover"
 | 
					 | 
				
			||||||
	@mouseleave="onMouseleave"
 | 
					 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<slot></slot>
 | 
						<slot></slot>
 | 
				
			||||||
	<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
 | 
						<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
 | 
				
			||||||
</component>
 | 
					</component>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import { url as local } from '@/config';
 | 
					import { url as local } from '@/config';
 | 
				
			||||||
import { isTouchUsing } from '@/scripts/touch';
 | 
					import { useTooltip } from '@/scripts/use-tooltip';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						url: string;
 | 
				
			||||||
		url: {
 | 
						rel?: null | string;
 | 
				
			||||||
			type: String,
 | 
					}>(), {
 | 
				
			||||||
			required: true,
 | 
					});
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		rel: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		const self = this.url.startsWith(local);
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			local,
 | 
					 | 
				
			||||||
			self: self,
 | 
					 | 
				
			||||||
			attr: self ? 'to' : 'href',
 | 
					 | 
				
			||||||
			target: self ? null : '_blank',
 | 
					 | 
				
			||||||
			showTimer: null,
 | 
					 | 
				
			||||||
			hideTimer: null,
 | 
					 | 
				
			||||||
			checkTimer: null,
 | 
					 | 
				
			||||||
			close: null,
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		async showPreview() {
 | 
					 | 
				
			||||||
			if (!document.body.contains(this.$el)) return;
 | 
					 | 
				
			||||||
			if (this.close) return;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), {
 | 
					const self = props.url.startsWith(local);
 | 
				
			||||||
				url: this.url,
 | 
					const attr = self ? 'to' : 'href';
 | 
				
			||||||
				source: this.$el
 | 
					const target = self ? null : '_blank';
 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.close = () => {
 | 
					const el = $ref();
 | 
				
			||||||
				dispose();
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.checkTimer = setInterval(() => {
 | 
					useTooltip($$(el), (showing) => {
 | 
				
			||||||
				if (!document.body.contains(this.$el)) this.closePreview();
 | 
						os.popup(import('@/components/url-preview-popup.vue'), {
 | 
				
			||||||
			}, 1000);
 | 
							showing,
 | 
				
			||||||
		},
 | 
							url: props.url,
 | 
				
			||||||
		closePreview() {
 | 
							source: el,
 | 
				
			||||||
			if (this.close) {
 | 
						}, {}, 'closed');
 | 
				
			||||||
				clearInterval(this.checkTimer);
 | 
					 | 
				
			||||||
				this.close();
 | 
					 | 
				
			||||||
				this.close = null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		onMouseover() {
 | 
					 | 
				
			||||||
			if (isTouchUsing) return;
 | 
					 | 
				
			||||||
			clearTimeout(this.showTimer);
 | 
					 | 
				
			||||||
			clearTimeout(this.hideTimer);
 | 
					 | 
				
			||||||
			this.showTimer = setTimeout(this.showPreview, 500);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		onMouseleave() {
 | 
					 | 
				
			||||||
			if (isTouchUsing) return;
 | 
					 | 
				
			||||||
			clearTimeout(this.showTimer);
 | 
					 | 
				
			||||||
			clearTimeout(this.hideTimer);
 | 
					 | 
				
			||||||
			this.hideTimer = setTimeout(this.closePreview, 500);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@
 | 
				
			||||||
		<span>{{ $ts.clickToShow }}</span>
 | 
							<span>{{ $ts.clickToShow }}</span>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
 | 
						<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
 | 
				
			||||||
		<audio ref="audio"
 | 
							<audio ref="audioEl"
 | 
				
			||||||
			class="audio"
 | 
								class="audio"
 | 
				
			||||||
			:src="media.url"
 | 
								:src="media.url"
 | 
				
			||||||
			:title="media.name"
 | 
								:title="media.name"
 | 
				
			||||||
| 
						 | 
					@ -25,34 +25,26 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { onMounted } from 'vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import { ColdDeviceStorage } from '@/store';
 | 
					import { ColdDeviceStorage } from '@/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						media: misskey.entities.DriveFile;
 | 
				
			||||||
		media: {
 | 
					}>(), {
 | 
				
			||||||
			type: Object,
 | 
					});
 | 
				
			||||||
			required: true
 | 
					
 | 
				
			||||||
		}
 | 
					const audioEl = $ref<HTMLAudioElement | null>();
 | 
				
			||||||
	},
 | 
					let hide = $ref(true);
 | 
				
			||||||
	data() {
 | 
					
 | 
				
			||||||
		return {
 | 
					function volumechange() {
 | 
				
			||||||
			hide: true,
 | 
						if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume);
 | 
				
			||||||
		};
 | 
					}
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					onMounted(() => {
 | 
				
			||||||
		const audioTag = this.$refs.audio as HTMLAudioElement;
 | 
						if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume');
 | 
				
			||||||
		if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
 | 
					});
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		volumechange() {
 | 
					 | 
				
			||||||
			const audioTag = this.$refs.audio as HTMLAudioElement;
 | 
					 | 
				
			||||||
			ColdDeviceStorage.set('mediaVolume', audioTag.volume);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -63,10 +63,10 @@ export default defineComponent({
 | 
				
			||||||
		this.draw();
 | 
							this.draw();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Vueが何故かWatchを発動させない場合があるので
 | 
							// Vueが何故かWatchを発動させない場合があるので
 | 
				
			||||||
		this.clock = setInterval(this.draw, 1000);
 | 
							this.clock = window.setInterval(this.draw, 1000);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	beforeUnmount() {
 | 
						beforeUnmount() {
 | 
				
			||||||
		clearInterval(this.clock);
 | 
							window.clearInterval(this.clock);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		draw() {
 | 
							draw() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,8 +8,8 @@
 | 
				
			||||||
	:tabindex="!isDeleted ? '-1' : null"
 | 
						:tabindex="!isDeleted ? '-1' : null"
 | 
				
			||||||
	:class="{ renote: isRenote }"
 | 
						:class="{ renote: isRenote }"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<XSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
 | 
						<MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
 | 
				
			||||||
	<XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
 | 
						<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
 | 
				
			||||||
	<div v-if="isRenote" class="renote">
 | 
						<div v-if="isRenote" class="renote">
 | 
				
			||||||
		<MkAvatar class="avatar" :user="note.user"/>
 | 
							<MkAvatar class="avatar" :user="note.user"/>
 | 
				
			||||||
		<i class="fas fa-retweet"></i>
 | 
							<i class="fas fa-retweet"></i>
 | 
				
			||||||
| 
						 | 
					@ -107,7 +107,7 @@
 | 
				
			||||||
			</footer>
 | 
								</footer>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</article>
 | 
						</article>
 | 
				
			||||||
	<XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
 | 
						<MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div v-else class="_panel muted" @click="muted = false">
 | 
					<div v-else class="_panel muted" @click="muted = false">
 | 
				
			||||||
	<I18n :src="$ts.userSaysSomething" tag="small">
 | 
						<I18n :src="$ts.userSaysSomething" tag="small">
 | 
				
			||||||
| 
						 | 
					@ -120,609 +120,109 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
 | 
					import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
 | 
				
			||||||
import * as mfm from 'mfm-js';
 | 
					import * as mfm from 'mfm-js';
 | 
				
			||||||
import { sum } from '@/scripts/array';
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import XSub from './note.sub.vue';
 | 
					import MkNoteSub from './MkNoteSub.vue';
 | 
				
			||||||
import XNoteHeader from './note-header.vue';
 | 
					 | 
				
			||||||
import XNoteSimple from './note-simple.vue';
 | 
					import XNoteSimple from './note-simple.vue';
 | 
				
			||||||
import XReactionsViewer from './reactions-viewer.vue';
 | 
					import XReactionsViewer from './reactions-viewer.vue';
 | 
				
			||||||
import XMediaList from './media-list.vue';
 | 
					import XMediaList from './media-list.vue';
 | 
				
			||||||
import XCwButton from './cw-button.vue';
 | 
					import XCwButton from './cw-button.vue';
 | 
				
			||||||
import XPoll from './poll.vue';
 | 
					import XPoll from './poll.vue';
 | 
				
			||||||
import XRenoteButton from './renote-button.vue';
 | 
					import XRenoteButton from './renote-button.vue';
 | 
				
			||||||
 | 
					import MkUrlPreview from '@/components/url-preview.vue';
 | 
				
			||||||
 | 
					import MkInstanceTicker from '@/components/instance-ticker.vue';
 | 
				
			||||||
import { pleaseLogin } from '@/scripts/please-login';
 | 
					import { pleaseLogin } from '@/scripts/please-login';
 | 
				
			||||||
import { focusPrev, focusNext } from '@/scripts/focus';
 | 
					 | 
				
			||||||
import { url } from '@/config';
 | 
					 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
					 | 
				
			||||||
import { checkWordMute } from '@/scripts/check-word-mute';
 | 
					import { checkWordMute } from '@/scripts/check-word-mute';
 | 
				
			||||||
import { userPage } from '@/filters/user';
 | 
					import { userPage } from '@/filters/user';
 | 
				
			||||||
import { notePage } from '@/filters/note';
 | 
					import { notePage } from '@/filters/note';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { stream } from '@/stream';
 | 
					import { defaultStore, noteViewInterruptors } from '@/store';
 | 
				
			||||||
import { noteActions, noteViewInterruptors } from '@/store';
 | 
					 | 
				
			||||||
import { reactionPicker } from '@/scripts/reaction-picker';
 | 
					import { reactionPicker } from '@/scripts/reaction-picker';
 | 
				
			||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
					import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
				
			||||||
 | 
					import { $i } from '@/account';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					import { getNoteMenu } from '@/scripts/get-note-menu';
 | 
				
			||||||
 | 
					import { useNoteCapture } from '@/scripts/use-note-capture';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TODO: note.vueとほぼ同じなので共通化したい
 | 
					const props = defineProps<{
 | 
				
			||||||
export default defineComponent({
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
	components: {
 | 
						pinned?: boolean;
 | 
				
			||||||
		XSub,
 | 
					}>();
 | 
				
			||||||
		XNoteHeader,
 | 
					 | 
				
			||||||
		XNoteSimple,
 | 
					 | 
				
			||||||
		XReactionsViewer,
 | 
					 | 
				
			||||||
		XMediaList,
 | 
					 | 
				
			||||||
		XCwButton,
 | 
					 | 
				
			||||||
		XPoll,
 | 
					 | 
				
			||||||
		XRenoteButton,
 | 
					 | 
				
			||||||
		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
 | 
					 | 
				
			||||||
		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	inject: {
 | 
					const inChannel = inject('inChannel', null);
 | 
				
			||||||
		inChannel: {
 | 
					 | 
				
			||||||
			default: null
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
					const isRenote = (
 | 
				
			||||||
		note: {
 | 
						props.note.renote != null &&
 | 
				
			||||||
			type: Object,
 | 
						props.note.text == null &&
 | 
				
			||||||
			required: true
 | 
						props.note.fileIds.length === 0 &&
 | 
				
			||||||
		},
 | 
						props.note.poll == null
 | 
				
			||||||
	},
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emits: ['update:note'],
 | 
					const el = ref<HTMLElement>();
 | 
				
			||||||
 | 
					const menuButton = ref<HTMLElement>();
 | 
				
			||||||
 | 
					const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
 | 
				
			||||||
 | 
					const renoteTime = ref<HTMLElement>();
 | 
				
			||||||
 | 
					const reactButton = ref<HTMLElement>();
 | 
				
			||||||
 | 
					let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
 | 
				
			||||||
 | 
					const isMyRenote = $i && ($i.id === props.note.userId);
 | 
				
			||||||
 | 
					const showContent = ref(false);
 | 
				
			||||||
 | 
					const isDeleted = ref(false);
 | 
				
			||||||
 | 
					const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
 | 
				
			||||||
 | 
					const translation = ref(null);
 | 
				
			||||||
 | 
					const translating = ref(false);
 | 
				
			||||||
 | 
					const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
 | 
				
			||||||
 | 
					const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
 | 
				
			||||||
 | 
					const conversation = ref<misskey.entities.Note[]>([]);
 | 
				
			||||||
 | 
					const replies = ref<misskey.entities.Note[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					const keymap = {
 | 
				
			||||||
		return {
 | 
						'r': () => reply(true),
 | 
				
			||||||
			connection: null,
 | 
						'e|a|plus': () => react(true),
 | 
				
			||||||
			conversation: [],
 | 
						'q': () => renoteButton.value.renote(true),
 | 
				
			||||||
			replies: [],
 | 
						'esc': blur,
 | 
				
			||||||
			showContent: false,
 | 
						'm|o': () => menu(true),
 | 
				
			||||||
			isDeleted: false,
 | 
						's': () => showContent.value != showContent.value,
 | 
				
			||||||
			muted: false,
 | 
					};
 | 
				
			||||||
			translation: null,
 | 
					 | 
				
			||||||
			translating: false,
 | 
					 | 
				
			||||||
			notePage,
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
					useNoteCapture({
 | 
				
			||||||
		rs() {
 | 
						appearNote: $$(appearNote),
 | 
				
			||||||
			return this.$store.state.reactions;
 | 
						rootEl: el,
 | 
				
			||||||
		},
 | 
					});
 | 
				
			||||||
		keymap(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				'r': () => this.reply(true),
 | 
					 | 
				
			||||||
				'e|a|plus': () => this.react(true),
 | 
					 | 
				
			||||||
				'q': () => this.$refs.renoteButton.renote(true),
 | 
					 | 
				
			||||||
				'f|b': this.favorite,
 | 
					 | 
				
			||||||
				'delete|ctrl+d': this.del,
 | 
					 | 
				
			||||||
				'ctrl+q': this.renoteDirectly,
 | 
					 | 
				
			||||||
				'up|k|shift+tab': this.focusBefore,
 | 
					 | 
				
			||||||
				'down|j|tab': this.focusAfter,
 | 
					 | 
				
			||||||
				'esc': this.blur,
 | 
					 | 
				
			||||||
				'm|o': () => this.menu(true),
 | 
					 | 
				
			||||||
				's': this.toggleShowContent,
 | 
					 | 
				
			||||||
				'1': () => this.reactDirectly(this.rs[0]),
 | 
					 | 
				
			||||||
				'2': () => this.reactDirectly(this.rs[1]),
 | 
					 | 
				
			||||||
				'3': () => this.reactDirectly(this.rs[2]),
 | 
					 | 
				
			||||||
				'4': () => this.reactDirectly(this.rs[3]),
 | 
					 | 
				
			||||||
				'5': () => this.reactDirectly(this.rs[4]),
 | 
					 | 
				
			||||||
				'6': () => this.reactDirectly(this.rs[5]),
 | 
					 | 
				
			||||||
				'7': () => this.reactDirectly(this.rs[6]),
 | 
					 | 
				
			||||||
				'8': () => this.reactDirectly(this.rs[7]),
 | 
					 | 
				
			||||||
				'9': () => this.reactDirectly(this.rs[8]),
 | 
					 | 
				
			||||||
				'0': () => this.reactDirectly(this.rs[9]),
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		isRenote(): boolean {
 | 
					function reply(viaKeyboard = false): void {
 | 
				
			||||||
			return (this.note.renote &&
 | 
					 | 
				
			||||||
				this.note.text == null &&
 | 
					 | 
				
			||||||
				this.note.fileIds.length == 0 &&
 | 
					 | 
				
			||||||
				this.note.poll == null);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		appearNote(): any {
 | 
					 | 
				
			||||||
			return this.isRenote ? this.note.renote : this.note;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		isMyNote(): boolean {
 | 
					 | 
				
			||||||
			return this.$i && (this.$i.id === this.appearNote.userId);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		isMyRenote(): boolean {
 | 
					 | 
				
			||||||
			return this.$i && (this.$i.id === this.note.userId);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reactionsCount(): number {
 | 
					 | 
				
			||||||
			return this.appearNote.reactions
 | 
					 | 
				
			||||||
				? sum(Object.values(this.appearNote.reactions))
 | 
					 | 
				
			||||||
				: 0;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		urls(): string[] {
 | 
					 | 
				
			||||||
			if (this.appearNote.text) {
 | 
					 | 
				
			||||||
				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				return null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		showTicker() {
 | 
					 | 
				
			||||||
			if (this.$store.state.instanceTicker === 'always') return true;
 | 
					 | 
				
			||||||
			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
 | 
					 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async created() {
 | 
					 | 
				
			||||||
		if (this.$i) {
 | 
					 | 
				
			||||||
			this.connection = stream;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// plugin
 | 
					 | 
				
			||||||
		if (noteViewInterruptors.length > 0) {
 | 
					 | 
				
			||||||
			let result = this.note;
 | 
					 | 
				
			||||||
			for (const interruptor of noteViewInterruptors) {
 | 
					 | 
				
			||||||
				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			this.$emit('update:note', Object.freeze(result));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		os.api('notes/children', {
 | 
					 | 
				
			||||||
			noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
			limit: 30
 | 
					 | 
				
			||||||
		}).then(replies => {
 | 
					 | 
				
			||||||
			this.replies = replies;
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (this.appearNote.replyId) {
 | 
					 | 
				
			||||||
			os.api('notes/conversation', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.replyId
 | 
					 | 
				
			||||||
			}).then(conversation => {
 | 
					 | 
				
			||||||
				this.conversation = conversation.reverse();
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.capture(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (this.$i) {
 | 
					 | 
				
			||||||
			this.connection.on('_connected_', this.onStreamConnected);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	beforeUnmount() {
 | 
					 | 
				
			||||||
		this.decapture(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (this.$i) {
 | 
					 | 
				
			||||||
			this.connection.off('_connected_', this.onStreamConnected);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		updateAppearNote(v) {
 | 
					 | 
				
			||||||
			this.$emit('update:note', Object.freeze(this.isRenote ? {
 | 
					 | 
				
			||||||
				...this.note,
 | 
					 | 
				
			||||||
				renote: {
 | 
					 | 
				
			||||||
					...this.note.renote,
 | 
					 | 
				
			||||||
					...v
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} : {
 | 
					 | 
				
			||||||
				...this.note,
 | 
					 | 
				
			||||||
				...v
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		readPromo() {
 | 
					 | 
				
			||||||
			os.api('promo/read', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			this.isDeleted = true;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		capture(withHandler = false) {
 | 
					 | 
				
			||||||
			if (this.$i) {
 | 
					 | 
				
			||||||
				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
 | 
					 | 
				
			||||||
				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
 | 
					 | 
				
			||||||
				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		decapture(withHandler = false) {
 | 
					 | 
				
			||||||
			if (this.$i) {
 | 
					 | 
				
			||||||
				this.connection.send('un', {
 | 
					 | 
				
			||||||
					id: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onStreamConnected() {
 | 
					 | 
				
			||||||
			this.capture();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onStreamNoteUpdated(data) {
 | 
					 | 
				
			||||||
			const { type, id, body } = data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (id !== this.appearNote.id) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			switch (type) {
 | 
					 | 
				
			||||||
				case 'reacted': {
 | 
					 | 
				
			||||||
					const reaction = body.reaction;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
 | 
					 | 
				
			||||||
					let n = {
 | 
					 | 
				
			||||||
						...this.appearNote,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (body.emoji) {
 | 
					 | 
				
			||||||
						const emojis = this.appearNote.emojis || [];
 | 
					 | 
				
			||||||
						if (!emojis.includes(body.emoji)) {
 | 
					 | 
				
			||||||
							n.emojis = [...emojis, body.emoji];
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
 | 
					 | 
				
			||||||
					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Increment the count
 | 
					 | 
				
			||||||
					n.reactions = {
 | 
					 | 
				
			||||||
						...this.appearNote.reactions,
 | 
					 | 
				
			||||||
						[reaction]: currentCount + 1
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (body.userId === this.$i.id) {
 | 
					 | 
				
			||||||
						n.myReaction = reaction;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.updateAppearNote(n);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'unreacted': {
 | 
					 | 
				
			||||||
					const reaction = body.reaction;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
 | 
					 | 
				
			||||||
					let n = {
 | 
					 | 
				
			||||||
						...this.appearNote,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
 | 
					 | 
				
			||||||
					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Decrement the count
 | 
					 | 
				
			||||||
					n.reactions = {
 | 
					 | 
				
			||||||
						...this.appearNote.reactions,
 | 
					 | 
				
			||||||
						[reaction]: Math.max(0, currentCount - 1)
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (body.userId === this.$i.id) {
 | 
					 | 
				
			||||||
						n.myReaction = null;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.updateAppearNote(n);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'pollVoted': {
 | 
					 | 
				
			||||||
					const choice = body.choice;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
 | 
					 | 
				
			||||||
					let n = {
 | 
					 | 
				
			||||||
						...this.appearNote,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					const choices = [...this.appearNote.poll.choices];
 | 
					 | 
				
			||||||
					choices[choice] = {
 | 
					 | 
				
			||||||
						...choices[choice],
 | 
					 | 
				
			||||||
						votes: choices[choice].votes + 1,
 | 
					 | 
				
			||||||
						...(body.userId === this.$i.id ? {
 | 
					 | 
				
			||||||
							isVoted: true
 | 
					 | 
				
			||||||
						} : {})
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					n.poll = {
 | 
					 | 
				
			||||||
						...this.appearNote.poll,
 | 
					 | 
				
			||||||
						choices: choices
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.updateAppearNote(n);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'deleted': {
 | 
					 | 
				
			||||||
					this.isDeleted = true;
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reply(viaKeyboard = false) {
 | 
					 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin();
 | 
				
			||||||
	os.post({
 | 
						os.post({
 | 
				
			||||||
				reply: this.appearNote,
 | 
							reply: appearNote,
 | 
				
			||||||
		animation: !viaKeyboard,
 | 
							animation: !viaKeyboard,
 | 
				
			||||||
	}, () => {
 | 
						}, () => {
 | 
				
			||||||
				this.focus();
 | 
							focus();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		renoteDirectly() {
 | 
					function react(viaKeyboard = false): void {
 | 
				
			||||||
			os.apiWithDialog('notes/create', {
 | 
					 | 
				
			||||||
				renoteId: this.appearNote.id
 | 
					 | 
				
			||||||
			}, undefined, (res: any) => {
 | 
					 | 
				
			||||||
				os.alert({
 | 
					 | 
				
			||||||
					type: 'success',
 | 
					 | 
				
			||||||
					text: this.$ts.renoted,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}, (e: Error) => {
 | 
					 | 
				
			||||||
				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.cantRenote,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.cantReRenote,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		react(viaKeyboard = false) {
 | 
					 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin();
 | 
				
			||||||
			this.blur();
 | 
						blur();
 | 
				
			||||||
			reactionPicker.show(this.$refs.reactButton, reaction => {
 | 
						reactionPicker.show(reactButton.value, reaction => {
 | 
				
			||||||
		os.api('notes/reactions/create', {
 | 
							os.api('notes/reactions/create', {
 | 
				
			||||||
					noteId: this.appearNote.id,
 | 
								noteId: appearNote.id,
 | 
				
			||||||
			reaction: reaction
 | 
								reaction: reaction
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}, () => {
 | 
						}, () => {
 | 
				
			||||||
				this.focus();
 | 
							focus();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		reactDirectly(reaction) {
 | 
					function undoReact(note): void {
 | 
				
			||||||
			os.api('notes/reactions/create', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
				reaction: reaction
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		undoReact(note) {
 | 
					 | 
				
			||||||
	const oldReaction = note.myReaction;
 | 
						const oldReaction = note.myReaction;
 | 
				
			||||||
	if (!oldReaction) return;
 | 
						if (!oldReaction) return;
 | 
				
			||||||
	os.api('notes/reactions/delete', {
 | 
						os.api('notes/reactions/delete', {
 | 
				
			||||||
		noteId: note.id
 | 
							noteId: note.id
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		favorite() {
 | 
					function onContextmenu(e): void {
 | 
				
			||||||
			pleaseLogin();
 | 
					 | 
				
			||||||
			os.apiWithDialog('notes/favorites/create', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			}, undefined, (res: any) => {
 | 
					 | 
				
			||||||
				os.alert({
 | 
					 | 
				
			||||||
					type: 'success',
 | 
					 | 
				
			||||||
					text: this.$ts.favorited,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}, (e: Error) => {
 | 
					 | 
				
			||||||
				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.alreadyFavorited,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.cantFavorite,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		del() {
 | 
					 | 
				
			||||||
			os.confirm({
 | 
					 | 
				
			||||||
				type: 'warning',
 | 
					 | 
				
			||||||
				text: this.$ts.noteDeleteConfirm,
 | 
					 | 
				
			||||||
			}).then(({ canceled }) => {
 | 
					 | 
				
			||||||
				if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				os.api('notes/delete', {
 | 
					 | 
				
			||||||
					noteId: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		delEdit() {
 | 
					 | 
				
			||||||
			os.confirm({
 | 
					 | 
				
			||||||
				type: 'warning',
 | 
					 | 
				
			||||||
				text: this.$ts.deleteAndEditConfirm,
 | 
					 | 
				
			||||||
			}).then(({ canceled }) => {
 | 
					 | 
				
			||||||
				if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				os.api('notes/delete', {
 | 
					 | 
				
			||||||
					noteId: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toggleFavorite(favorite: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toggleWatch(watch: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toggleThreadMute(mute: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		getMenu() {
 | 
					 | 
				
			||||||
			let menu;
 | 
					 | 
				
			||||||
			if (this.$i) {
 | 
					 | 
				
			||||||
				const statePromise = os.api('notes/state', {
 | 
					 | 
				
			||||||
					noteId: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				menu = [{
 | 
					 | 
				
			||||||
					icon: 'fas fa-copy',
 | 
					 | 
				
			||||||
					text: this.$ts.copyContent,
 | 
					 | 
				
			||||||
					action: this.copyContent
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					icon: 'fas fa-link',
 | 
					 | 
				
			||||||
					text: this.$ts.copyLink,
 | 
					 | 
				
			||||||
					action: this.copyLink
 | 
					 | 
				
			||||||
				}, (this.appearNote.url || this.appearNote.uri) ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-external-link-square-alt',
 | 
					 | 
				
			||||||
					text: this.$ts.showOnRemote,
 | 
					 | 
				
			||||||
					action: () => {
 | 
					 | 
				
			||||||
						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} : undefined,
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					icon: 'fas fa-share-alt',
 | 
					 | 
				
			||||||
					text: this.$ts.share,
 | 
					 | 
				
			||||||
					action: this.share
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				this.$instance.translatorAvailable ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-language',
 | 
					 | 
				
			||||||
					text: this.$ts.translate,
 | 
					 | 
				
			||||||
					action: this.translate
 | 
					 | 
				
			||||||
				} : undefined,
 | 
					 | 
				
			||||||
				null,
 | 
					 | 
				
			||||||
				statePromise.then(state => state.isFavorited ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-star',
 | 
					 | 
				
			||||||
					text: this.$ts.unfavorite,
 | 
					 | 
				
			||||||
					action: () => this.toggleFavorite(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-star',
 | 
					 | 
				
			||||||
					text: this.$ts.favorite,
 | 
					 | 
				
			||||||
					action: () => this.toggleFavorite(true)
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					icon: 'fas fa-paperclip',
 | 
					 | 
				
			||||||
					text: this.$ts.clip,
 | 
					 | 
				
			||||||
					action: () => this.clip()
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-eye-slash',
 | 
					 | 
				
			||||||
					text: this.$ts.unwatch,
 | 
					 | 
				
			||||||
					action: () => this.toggleWatch(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-eye',
 | 
					 | 
				
			||||||
					text: this.$ts.watch,
 | 
					 | 
				
			||||||
					action: () => this.toggleWatch(true)
 | 
					 | 
				
			||||||
				}) : undefined,
 | 
					 | 
				
			||||||
				statePromise.then(state => state.isMutedThread ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-comment-slash',
 | 
					 | 
				
			||||||
					text: this.$ts.unmuteThread,
 | 
					 | 
				
			||||||
					action: () => this.toggleThreadMute(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-comment-slash',
 | 
					 | 
				
			||||||
					text: this.$ts.muteThread,
 | 
					 | 
				
			||||||
					action: () => this.toggleThreadMute(true)
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-thumbtack',
 | 
					 | 
				
			||||||
					text: this.$ts.unpin,
 | 
					 | 
				
			||||||
					action: () => this.togglePin(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-thumbtack',
 | 
					 | 
				
			||||||
					text: this.$ts.pin,
 | 
					 | 
				
			||||||
					action: () => this.togglePin(true)
 | 
					 | 
				
			||||||
				} : undefined,
 | 
					 | 
				
			||||||
				/*...(this.$i.isModerator || this.$i.isAdmin ? [
 | 
					 | 
				
			||||||
					null,
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						icon: 'fas fa-bullhorn',
 | 
					 | 
				
			||||||
						text: this.$ts.promote,
 | 
					 | 
				
			||||||
						action: this.promote
 | 
					 | 
				
			||||||
					}]
 | 
					 | 
				
			||||||
					: []
 | 
					 | 
				
			||||||
				),*/
 | 
					 | 
				
			||||||
				...(this.appearNote.userId != this.$i.id ? [
 | 
					 | 
				
			||||||
					null,
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						icon: 'fas fa-exclamation-circle',
 | 
					 | 
				
			||||||
						text: this.$ts.reportAbuse,
 | 
					 | 
				
			||||||
						action: () => {
 | 
					 | 
				
			||||||
							const u = `${url}/notes/${this.appearNote.id}`;
 | 
					 | 
				
			||||||
							os.popup(import('@/components/abuse-report-window.vue'), {
 | 
					 | 
				
			||||||
								user: this.appearNote.user,
 | 
					 | 
				
			||||||
								initialComment: `Note: ${u}\n-----\n`
 | 
					 | 
				
			||||||
							}, {}, 'closed');
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}]
 | 
					 | 
				
			||||||
					: []
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
 | 
					 | 
				
			||||||
					null,
 | 
					 | 
				
			||||||
					this.appearNote.userId == this.$i.id ? {
 | 
					 | 
				
			||||||
						icon: 'fas fa-edit',
 | 
					 | 
				
			||||||
						text: this.$ts.deleteAndEdit,
 | 
					 | 
				
			||||||
						action: this.delEdit
 | 
					 | 
				
			||||||
					} : undefined,
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						icon: 'fas fa-trash-alt',
 | 
					 | 
				
			||||||
						text: this.$ts.delete,
 | 
					 | 
				
			||||||
						danger: true,
 | 
					 | 
				
			||||||
						action: this.del
 | 
					 | 
				
			||||||
					}]
 | 
					 | 
				
			||||||
					: []
 | 
					 | 
				
			||||||
				)]
 | 
					 | 
				
			||||||
				.filter(x => x !== undefined);
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				menu = [{
 | 
					 | 
				
			||||||
					icon: 'fas fa-copy',
 | 
					 | 
				
			||||||
					text: this.$ts.copyContent,
 | 
					 | 
				
			||||||
					action: this.copyContent
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					icon: 'fas fa-link',
 | 
					 | 
				
			||||||
					text: this.$ts.copyLink,
 | 
					 | 
				
			||||||
					action: this.copyLink
 | 
					 | 
				
			||||||
				}, (this.appearNote.url || this.appearNote.uri) ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-external-link-square-alt',
 | 
					 | 
				
			||||||
					text: this.$ts.showOnRemote,
 | 
					 | 
				
			||||||
					action: () => {
 | 
					 | 
				
			||||||
						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} : undefined]
 | 
					 | 
				
			||||||
				.filter(x => x !== undefined);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (noteActions.length > 0) {
 | 
					 | 
				
			||||||
				menu = menu.concat([null, ...noteActions.map(action => ({
 | 
					 | 
				
			||||||
					icon: 'fas fa-plug',
 | 
					 | 
				
			||||||
					text: action.title,
 | 
					 | 
				
			||||||
					action: () => {
 | 
					 | 
				
			||||||
						action.handler(this.appearNote);
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}))]);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return menu;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onContextmenu(e) {
 | 
					 | 
				
			||||||
	const isLink = (el: HTMLElement) => {
 | 
						const isLink = (el: HTMLElement) => {
 | 
				
			||||||
		if (el.tagName === 'A') return true;
 | 
							if (el.tagName === 'A') return true;
 | 
				
			||||||
		if (el.parentElement) {
 | 
							if (el.parentElement) {
 | 
				
			||||||
| 
						 | 
					@ -732,153 +232,59 @@ export default defineComponent({
 | 
				
			||||||
	if (isLink(e.target)) return;
 | 
						if (isLink(e.target)) return;
 | 
				
			||||||
	if (window.getSelection().toString() !== '') return;
 | 
						if (window.getSelection().toString() !== '') return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.$store.state.useReactionPickerForContextMenu) {
 | 
						if (defaultStore.state.useReactionPickerForContextMenu) {
 | 
				
			||||||
		e.preventDefault();
 | 
							e.preventDefault();
 | 
				
			||||||
				this.react();
 | 
							react();
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
				os.contextMenu(this.getMenu(), e).then(this.focus);
 | 
							os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		menu(viaKeyboard = false) {
 | 
					function menu(viaKeyboard = false): void {
 | 
				
			||||||
			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
 | 
						os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
 | 
				
			||||||
		viaKeyboard
 | 
							viaKeyboard
 | 
				
			||||||
			}).then(this.focus);
 | 
						}).then(focus);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		showRenoteMenu(viaKeyboard = false) {
 | 
					function showRenoteMenu(viaKeyboard = false): void {
 | 
				
			||||||
			if (!this.isMyRenote) return;
 | 
						if (!isMyRenote) return;
 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
				text: this.$ts.unrenote,
 | 
							text: i18n.locale.unrenote,
 | 
				
			||||||
		icon: 'fas fa-trash-alt',
 | 
							icon: 'fas fa-trash-alt',
 | 
				
			||||||
		danger: true,
 | 
							danger: true,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
			os.api('notes/delete', {
 | 
								os.api('notes/delete', {
 | 
				
			||||||
						noteId: this.note.id
 | 
									noteId: props.note.id
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
					this.isDeleted = true;
 | 
								isDeleted.value = true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			}], this.$refs.renoteTime, {
 | 
						}], renoteTime.value, {
 | 
				
			||||||
		viaKeyboard: viaKeyboard
 | 
							viaKeyboard: viaKeyboard
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		toggleShowContent() {
 | 
					function focus() {
 | 
				
			||||||
			this.showContent = !this.showContent;
 | 
						el.value.focus();
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		copyContent() {
 | 
					function blur() {
 | 
				
			||||||
			copyToClipboard(this.appearNote.text);
 | 
						el.value.blur();
 | 
				
			||||||
			os.success();
 | 
					}
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		copyLink() {
 | 
					os.api('notes/children', {
 | 
				
			||||||
			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
 | 
						noteId: appearNote.id,
 | 
				
			||||||
			os.success();
 | 
						limit: 30
 | 
				
			||||||
		},
 | 
					}).then(res => {
 | 
				
			||||||
 | 
						replies.value = res;
 | 
				
			||||||
		togglePin(pin: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			}, undefined, null, e => {
 | 
					 | 
				
			||||||
				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.pinLimitExceeded
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		async clip() {
 | 
					 | 
				
			||||||
			const clips = await os.api('clips/list');
 | 
					 | 
				
			||||||
			os.popupMenu([{
 | 
					 | 
				
			||||||
				icon: 'fas fa-plus',
 | 
					 | 
				
			||||||
				text: this.$ts.createNew,
 | 
					 | 
				
			||||||
				action: async () => {
 | 
					 | 
				
			||||||
					const { canceled, result } = await os.form(this.$ts.createNewClip, {
 | 
					 | 
				
			||||||
						name: {
 | 
					 | 
				
			||||||
							type: 'string',
 | 
					 | 
				
			||||||
							label: this.$ts.name
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						description: {
 | 
					 | 
				
			||||||
							type: 'string',
 | 
					 | 
				
			||||||
							required: false,
 | 
					 | 
				
			||||||
							multiline: true,
 | 
					 | 
				
			||||||
							label: this.$ts.description
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						isPublic: {
 | 
					 | 
				
			||||||
							type: 'boolean',
 | 
					 | 
				
			||||||
							label: this.$ts.public,
 | 
					 | 
				
			||||||
							default: false
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
					if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					const clip = await os.apiWithDialog('clips/create', result);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}, null, ...clips.map(clip => ({
 | 
					 | 
				
			||||||
				text: clip.name,
 | 
					 | 
				
			||||||
				action: () => {
 | 
					 | 
				
			||||||
					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}))], this.$refs.menuButton, {
 | 
					 | 
				
			||||||
			}).then(this.focus);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		async promote() {
 | 
					 | 
				
			||||||
			const { canceled, result: days } = await os.inputNumber({
 | 
					 | 
				
			||||||
				title: this.$ts.numberOfDays,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			os.apiWithDialog('admin/promo/create', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
				expiresAt: Date.now() + (86400000 * days)
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		share() {
 | 
					 | 
				
			||||||
			navigator.share({
 | 
					 | 
				
			||||||
				title: this.$t('noteOf', { user: this.appearNote.user.name }),
 | 
					 | 
				
			||||||
				text: this.appearNote.text,
 | 
					 | 
				
			||||||
				url: `${url}/notes/${this.appearNote.id}`
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		async translate() {
 | 
					 | 
				
			||||||
			if (this.translation != null) return;
 | 
					 | 
				
			||||||
			this.translating = true;
 | 
					 | 
				
			||||||
			const res = await os.api('notes/translate', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
				targetLang: localStorage.getItem('lang') || navigator.language,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			this.translating = false;
 | 
					 | 
				
			||||||
			this.translation = res;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		focus() {
 | 
					 | 
				
			||||||
			this.$el.focus();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		blur() {
 | 
					 | 
				
			||||||
			this.$el.blur();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		focusBefore() {
 | 
					 | 
				
			||||||
			focusPrev(this.$el);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		focusAfter() {
 | 
					 | 
				
			||||||
			focusNext(this.$el);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		userPage
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (appearNote.replyId) {
 | 
				
			||||||
 | 
						os.api('notes/conversation', {
 | 
				
			||||||
 | 
							noteId: appearNote.replyId
 | 
				
			||||||
 | 
						}).then(res => {
 | 
				
			||||||
 | 
							conversation.value = res.reverse();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,30 +19,16 @@
 | 
				
			||||||
</header>
 | 
					</header>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import { notePage } from '@/filters/note';
 | 
					import { notePage } from '@/filters/note';
 | 
				
			||||||
import { userPage } from '@/filters/user';
 | 
					import { userPage } from '@/filters/user';
 | 
				
			||||||
import * as os from '@/os';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					defineProps<{
 | 
				
			||||||
	props: {
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
		note: {
 | 
						pinned?: boolean;
 | 
				
			||||||
			type: Object,
 | 
					}>();
 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		notePage,
 | 
					 | 
				
			||||||
		userPage
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,20 +14,12 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						text: string;
 | 
				
			||||||
	},
 | 
					}>();
 | 
				
			||||||
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		text: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,40 +9,26 @@
 | 
				
			||||||
				<XCwButton v-model="showContent" :note="note"/>
 | 
									<XCwButton v-model="showContent" :note="note"/>
 | 
				
			||||||
			</p>
 | 
								</p>
 | 
				
			||||||
			<div v-show="note.cw == null || showContent" class="content">
 | 
								<div v-show="note.cw == null || showContent" class="content">
 | 
				
			||||||
				<XSubNote-content class="text" :note="note"/>
 | 
									<MkNoteSubNoteContent class="text" :note="note"/>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import XNoteHeader from './note-header.vue';
 | 
					import XNoteHeader from './note-header.vue';
 | 
				
			||||||
import XSubNoteContent from './sub-note-content.vue';
 | 
					import MkNoteSubNoteContent from './sub-note-content.vue';
 | 
				
			||||||
import XCwButton from './cw-button.vue';
 | 
					import XCwButton from './cw-button.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
		XNoteHeader,
 | 
						pinned?: boolean;
 | 
				
			||||||
		XSubNoteContent,
 | 
					}>();
 | 
				
			||||||
		XCwButton,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
					const showContent = $ref(false);
 | 
				
			||||||
		note: {
 | 
					 | 
				
			||||||
			type: Object,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			showContent: false
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,20 +2,21 @@
 | 
				
			||||||
<div
 | 
					<div
 | 
				
			||||||
	v-if="!muted"
 | 
						v-if="!muted"
 | 
				
			||||||
	v-show="!isDeleted"
 | 
						v-show="!isDeleted"
 | 
				
			||||||
 | 
						ref="el"
 | 
				
			||||||
	v-hotkey="keymap"
 | 
						v-hotkey="keymap"
 | 
				
			||||||
	v-size="{ max: [500, 450, 350, 300] }"
 | 
						v-size="{ max: [500, 450, 350, 300] }"
 | 
				
			||||||
	class="tkcbzcuz"
 | 
						class="tkcbzcuz"
 | 
				
			||||||
	:tabindex="!isDeleted ? '-1' : null"
 | 
						:tabindex="!isDeleted ? '-1' : null"
 | 
				
			||||||
	:class="{ renote: isRenote }"
 | 
						:class="{ renote: isRenote }"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
 | 
						<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
 | 
				
			||||||
	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
 | 
						<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div>
 | 
				
			||||||
	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
 | 
						<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div>
 | 
				
			||||||
	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
 | 
						<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div>
 | 
				
			||||||
	<div v-if="isRenote" class="renote">
 | 
						<div v-if="isRenote" class="renote">
 | 
				
			||||||
		<MkAvatar class="avatar" :user="note.user"/>
 | 
							<MkAvatar class="avatar" :user="note.user"/>
 | 
				
			||||||
		<i class="fas fa-retweet"></i>
 | 
							<i class="fas fa-retweet"></i>
 | 
				
			||||||
		<I18n :src="$ts.renotedBy" tag="span">
 | 
							<I18n :src="i18n.locale.renotedBy" tag="span">
 | 
				
			||||||
			<template #user>
 | 
								<template #user>
 | 
				
			||||||
				<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
 | 
									<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
 | 
				
			||||||
					<MkUserName :user="note.user"/>
 | 
										<MkUserName :user="note.user"/>
 | 
				
			||||||
| 
						 | 
					@ -47,7 +48,7 @@
 | 
				
			||||||
				</p>
 | 
									</p>
 | 
				
			||||||
				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
 | 
									<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
 | 
				
			||||||
					<div class="text">
 | 
										<div class="text">
 | 
				
			||||||
						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
 | 
											<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span>
 | 
				
			||||||
						<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
 | 
											<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
 | 
				
			||||||
						<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
 | 
											<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
 | 
				
			||||||
						<a v-if="appearNote.renote != null" class="rp">RN:</a>
 | 
											<a v-if="appearNote.renote != null" class="rp">RN:</a>
 | 
				
			||||||
| 
						 | 
					@ -66,7 +67,7 @@
 | 
				
			||||||
					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
 | 
										<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
 | 
				
			||||||
					<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
 | 
										<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
 | 
				
			||||||
					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
 | 
										<button v-if="collapsed" class="fade _button" @click="collapsed = false">
 | 
				
			||||||
						<span>{{ $ts.showMore }}</span>
 | 
											<span>{{ i18n.locale.showMore }}</span>
 | 
				
			||||||
					</button>
 | 
										</button>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
 | 
									<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
 | 
				
			||||||
| 
						 | 
					@ -93,7 +94,7 @@
 | 
				
			||||||
	</article>
 | 
						</article>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div v-else class="muted" @click="muted = false">
 | 
					<div v-else class="muted" @click="muted = false">
 | 
				
			||||||
	<I18n :src="$ts.userSaysSomething" tag="small">
 | 
						<I18n :src="i18n.locale.userSaysSomething" tag="small">
 | 
				
			||||||
		<template #name>
 | 
							<template #name>
 | 
				
			||||||
			<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
 | 
								<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
 | 
				
			||||||
				<MkUserName :user="appearNote.user"/>
 | 
									<MkUserName :user="appearNote.user"/>
 | 
				
			||||||
| 
						 | 
					@ -103,11 +104,11 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
 | 
					import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
 | 
				
			||||||
import * as mfm from 'mfm-js';
 | 
					import * as mfm from 'mfm-js';
 | 
				
			||||||
import { sum } from '@/scripts/array';
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import XSub from './note.sub.vue';
 | 
					import MkNoteSub from './MkNoteSub.vue';
 | 
				
			||||||
import XNoteHeader from './note-header.vue';
 | 
					import XNoteHeader from './note-header.vue';
 | 
				
			||||||
import XNoteSimple from './note-simple.vue';
 | 
					import XNoteSimple from './note-simple.vue';
 | 
				
			||||||
import XReactionsViewer from './reactions-viewer.vue';
 | 
					import XReactionsViewer from './reactions-viewer.vue';
 | 
				
			||||||
| 
						 | 
					@ -115,589 +116,102 @@ import XMediaList from './media-list.vue';
 | 
				
			||||||
import XCwButton from './cw-button.vue';
 | 
					import XCwButton from './cw-button.vue';
 | 
				
			||||||
import XPoll from './poll.vue';
 | 
					import XPoll from './poll.vue';
 | 
				
			||||||
import XRenoteButton from './renote-button.vue';
 | 
					import XRenoteButton from './renote-button.vue';
 | 
				
			||||||
 | 
					import MkUrlPreview from '@/components/url-preview.vue';
 | 
				
			||||||
 | 
					import MkInstanceTicker from '@/components/instance-ticker.vue';
 | 
				
			||||||
import { pleaseLogin } from '@/scripts/please-login';
 | 
					import { pleaseLogin } from '@/scripts/please-login';
 | 
				
			||||||
import { focusPrev, focusNext } from '@/scripts/focus';
 | 
					import { focusPrev, focusNext } from '@/scripts/focus';
 | 
				
			||||||
import { url } from '@/config';
 | 
					 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
					 | 
				
			||||||
import { checkWordMute } from '@/scripts/check-word-mute';
 | 
					import { checkWordMute } from '@/scripts/check-word-mute';
 | 
				
			||||||
import { userPage } from '@/filters/user';
 | 
					import { userPage } from '@/filters/user';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { stream } from '@/stream';
 | 
					import { defaultStore, noteViewInterruptors } from '@/store';
 | 
				
			||||||
import { noteActions, noteViewInterruptors } from '@/store';
 | 
					 | 
				
			||||||
import { reactionPicker } from '@/scripts/reaction-picker';
 | 
					import { reactionPicker } from '@/scripts/reaction-picker';
 | 
				
			||||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
					import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
 | 
				
			||||||
 | 
					import { $i } from '@/account';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					import { getNoteMenu } from '@/scripts/get-note-menu';
 | 
				
			||||||
 | 
					import { useNoteCapture } from '@/scripts/use-note-capture';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
		XSub,
 | 
						pinned?: boolean;
 | 
				
			||||||
		XNoteHeader,
 | 
					}>();
 | 
				
			||||||
		XNoteSimple,
 | 
					 | 
				
			||||||
		XReactionsViewer,
 | 
					 | 
				
			||||||
		XMediaList,
 | 
					 | 
				
			||||||
		XCwButton,
 | 
					 | 
				
			||||||
		XPoll,
 | 
					 | 
				
			||||||
		XRenoteButton,
 | 
					 | 
				
			||||||
		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
 | 
					 | 
				
			||||||
		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	inject: {
 | 
					const inChannel = inject('inChannel', null);
 | 
				
			||||||
		inChannel: {
 | 
					 | 
				
			||||||
			default: null
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
					const isRenote = (
 | 
				
			||||||
		note: {
 | 
						props.note.renote != null &&
 | 
				
			||||||
			type: Object,
 | 
						props.note.text == null &&
 | 
				
			||||||
			required: true
 | 
						props.note.fileIds.length === 0 &&
 | 
				
			||||||
		},
 | 
						props.note.poll == null
 | 
				
			||||||
		pinned: {
 | 
					);
 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emits: ['update:note'],
 | 
					const el = ref<HTMLElement>();
 | 
				
			||||||
 | 
					const menuButton = ref<HTMLElement>();
 | 
				
			||||||
 | 
					const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
 | 
				
			||||||
 | 
					const renoteTime = ref<HTMLElement>();
 | 
				
			||||||
 | 
					const reactButton = ref<HTMLElement>();
 | 
				
			||||||
 | 
					let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
 | 
				
			||||||
 | 
					const isMyRenote = $i && ($i.id === props.note.userId);
 | 
				
			||||||
 | 
					const showContent = ref(false);
 | 
				
			||||||
 | 
					const collapsed = ref(appearNote.cw == null && appearNote.text != null && (
 | 
				
			||||||
 | 
						(appearNote.text.split('\n').length > 9) ||
 | 
				
			||||||
 | 
						(appearNote.text.length > 500)
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					const isDeleted = ref(false);
 | 
				
			||||||
 | 
					const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
 | 
				
			||||||
 | 
					const translation = ref(null);
 | 
				
			||||||
 | 
					const translating = ref(false);
 | 
				
			||||||
 | 
					const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
 | 
				
			||||||
 | 
					const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					const keymap = {
 | 
				
			||||||
		return {
 | 
						'r': () => reply(true),
 | 
				
			||||||
			connection: null,
 | 
						'e|a|plus': () => react(true),
 | 
				
			||||||
			replies: [],
 | 
						'q': () => renoteButton.value.renote(true),
 | 
				
			||||||
			showContent: false,
 | 
						'up|k|shift+tab': focusBefore,
 | 
				
			||||||
			collapsed: false,
 | 
						'down|j|tab': focusAfter,
 | 
				
			||||||
			isDeleted: false,
 | 
						'esc': blur,
 | 
				
			||||||
			muted: false,
 | 
						'm|o': () => menu(true),
 | 
				
			||||||
			translation: null,
 | 
						's': () => showContent.value != showContent.value,
 | 
				
			||||||
			translating: false,
 | 
					};
 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
					useNoteCapture({
 | 
				
			||||||
		rs() {
 | 
						appearNote: $$(appearNote),
 | 
				
			||||||
			return this.$store.state.reactions;
 | 
						rootEl: el,
 | 
				
			||||||
		},
 | 
					});
 | 
				
			||||||
		keymap(): any {
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				'r': () => this.reply(true),
 | 
					 | 
				
			||||||
				'e|a|plus': () => this.react(true),
 | 
					 | 
				
			||||||
				'q': () => this.$refs.renoteButton.renote(true),
 | 
					 | 
				
			||||||
				'f|b': this.favorite,
 | 
					 | 
				
			||||||
				'delete|ctrl+d': this.del,
 | 
					 | 
				
			||||||
				'ctrl+q': this.renoteDirectly,
 | 
					 | 
				
			||||||
				'up|k|shift+tab': this.focusBefore,
 | 
					 | 
				
			||||||
				'down|j|tab': this.focusAfter,
 | 
					 | 
				
			||||||
				'esc': this.blur,
 | 
					 | 
				
			||||||
				'm|o': () => this.menu(true),
 | 
					 | 
				
			||||||
				's': this.toggleShowContent,
 | 
					 | 
				
			||||||
				'1': () => this.reactDirectly(this.rs[0]),
 | 
					 | 
				
			||||||
				'2': () => this.reactDirectly(this.rs[1]),
 | 
					 | 
				
			||||||
				'3': () => this.reactDirectly(this.rs[2]),
 | 
					 | 
				
			||||||
				'4': () => this.reactDirectly(this.rs[3]),
 | 
					 | 
				
			||||||
				'5': () => this.reactDirectly(this.rs[4]),
 | 
					 | 
				
			||||||
				'6': () => this.reactDirectly(this.rs[5]),
 | 
					 | 
				
			||||||
				'7': () => this.reactDirectly(this.rs[6]),
 | 
					 | 
				
			||||||
				'8': () => this.reactDirectly(this.rs[7]),
 | 
					 | 
				
			||||||
				'9': () => this.reactDirectly(this.rs[8]),
 | 
					 | 
				
			||||||
				'0': () => this.reactDirectly(this.rs[9]),
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		isRenote(): boolean {
 | 
					function reply(viaKeyboard = false): void {
 | 
				
			||||||
			return (this.note.renote &&
 | 
					 | 
				
			||||||
				this.note.text == null &&
 | 
					 | 
				
			||||||
				this.note.fileIds.length == 0 &&
 | 
					 | 
				
			||||||
				this.note.poll == null);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		appearNote(): any {
 | 
					 | 
				
			||||||
			return this.isRenote ? this.note.renote : this.note;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		isMyNote(): boolean {
 | 
					 | 
				
			||||||
			return this.$i && (this.$i.id === this.appearNote.userId);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		isMyRenote(): boolean {
 | 
					 | 
				
			||||||
			return this.$i && (this.$i.id === this.note.userId);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reactionsCount(): number {
 | 
					 | 
				
			||||||
			return this.appearNote.reactions
 | 
					 | 
				
			||||||
				? sum(Object.values(this.appearNote.reactions))
 | 
					 | 
				
			||||||
				: 0;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		urls(): string[] {
 | 
					 | 
				
			||||||
			if (this.appearNote.text) {
 | 
					 | 
				
			||||||
				return extractUrlFromMfm(mfm.parse(this.appearNote.text));
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				return null;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		showTicker() {
 | 
					 | 
				
			||||||
			if (this.$store.state.instanceTicker === 'always') return true;
 | 
					 | 
				
			||||||
			if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
 | 
					 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	async created() {
 | 
					 | 
				
			||||||
		if (this.$i) {
 | 
					 | 
				
			||||||
			this.connection = stream;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
 | 
					 | 
				
			||||||
			(this.appearNote.text.split('\n').length > 9) ||
 | 
					 | 
				
			||||||
			(this.appearNote.text.length > 500)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
		this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// plugin
 | 
					 | 
				
			||||||
		if (noteViewInterruptors.length > 0) {
 | 
					 | 
				
			||||||
			let result = this.note;
 | 
					 | 
				
			||||||
			for (const interruptor of noteViewInterruptors) {
 | 
					 | 
				
			||||||
				result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			this.$emit('update:note', Object.freeze(result));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.capture(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (this.$i) {
 | 
					 | 
				
			||||||
			this.connection.on('_connected_', this.onStreamConnected);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	beforeUnmount() {
 | 
					 | 
				
			||||||
		this.decapture(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (this.$i) {
 | 
					 | 
				
			||||||
			this.connection.off('_connected_', this.onStreamConnected);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		updateAppearNote(v) {
 | 
					 | 
				
			||||||
			this.$emit('update:note', Object.freeze(this.isRenote ? {
 | 
					 | 
				
			||||||
				...this.note,
 | 
					 | 
				
			||||||
				renote: {
 | 
					 | 
				
			||||||
					...this.note.renote,
 | 
					 | 
				
			||||||
					...v
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} : {
 | 
					 | 
				
			||||||
				...this.note,
 | 
					 | 
				
			||||||
				...v
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		readPromo() {
 | 
					 | 
				
			||||||
			os.api('promo/read', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			this.isDeleted = true;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		capture(withHandler = false) {
 | 
					 | 
				
			||||||
			if (this.$i) {
 | 
					 | 
				
			||||||
				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
 | 
					 | 
				
			||||||
				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
 | 
					 | 
				
			||||||
				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		decapture(withHandler = false) {
 | 
					 | 
				
			||||||
			if (this.$i) {
 | 
					 | 
				
			||||||
				this.connection.send('un', {
 | 
					 | 
				
			||||||
					id: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
				if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onStreamConnected() {
 | 
					 | 
				
			||||||
			this.capture();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onStreamNoteUpdated(data) {
 | 
					 | 
				
			||||||
			const { type, id, body } = data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (id !== this.appearNote.id) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			switch (type) {
 | 
					 | 
				
			||||||
				case 'reacted': {
 | 
					 | 
				
			||||||
					const reaction = body.reaction;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
 | 
					 | 
				
			||||||
					let n = {
 | 
					 | 
				
			||||||
						...this.appearNote,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (body.emoji) {
 | 
					 | 
				
			||||||
						const emojis = this.appearNote.emojis || [];
 | 
					 | 
				
			||||||
						if (!emojis.includes(body.emoji)) {
 | 
					 | 
				
			||||||
							n.emojis = [...emojis, body.emoji];
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
 | 
					 | 
				
			||||||
					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Increment the count
 | 
					 | 
				
			||||||
					n.reactions = {
 | 
					 | 
				
			||||||
						...this.appearNote.reactions,
 | 
					 | 
				
			||||||
						[reaction]: currentCount + 1
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (body.userId === this.$i.id) {
 | 
					 | 
				
			||||||
						n.myReaction = reaction;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.updateAppearNote(n);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'unreacted': {
 | 
					 | 
				
			||||||
					const reaction = body.reaction;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
 | 
					 | 
				
			||||||
					let n = {
 | 
					 | 
				
			||||||
						...this.appearNote,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
 | 
					 | 
				
			||||||
					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// Decrement the count
 | 
					 | 
				
			||||||
					n.reactions = {
 | 
					 | 
				
			||||||
						...this.appearNote.reactions,
 | 
					 | 
				
			||||||
						[reaction]: Math.max(0, currentCount - 1)
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					if (body.userId === this.$i.id) {
 | 
					 | 
				
			||||||
						n.myReaction = null;
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.updateAppearNote(n);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'pollVoted': {
 | 
					 | 
				
			||||||
					const choice = body.choice;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
 | 
					 | 
				
			||||||
					let n = {
 | 
					 | 
				
			||||||
						...this.appearNote,
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					const choices = [...this.appearNote.poll.choices];
 | 
					 | 
				
			||||||
					choices[choice] = {
 | 
					 | 
				
			||||||
						...choices[choice],
 | 
					 | 
				
			||||||
						votes: choices[choice].votes + 1,
 | 
					 | 
				
			||||||
						...(body.userId === this.$i.id ? {
 | 
					 | 
				
			||||||
							isVoted: true
 | 
					 | 
				
			||||||
						} : {})
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					n.poll = {
 | 
					 | 
				
			||||||
						...this.appearNote.poll,
 | 
					 | 
				
			||||||
						choices: choices
 | 
					 | 
				
			||||||
					};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					this.updateAppearNote(n);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				case 'deleted': {
 | 
					 | 
				
			||||||
					this.isDeleted = true;
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		reply(viaKeyboard = false) {
 | 
					 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin();
 | 
				
			||||||
	os.post({
 | 
						os.post({
 | 
				
			||||||
				reply: this.appearNote,
 | 
							reply: appearNote,
 | 
				
			||||||
		animation: !viaKeyboard,
 | 
							animation: !viaKeyboard,
 | 
				
			||||||
	}, () => {
 | 
						}, () => {
 | 
				
			||||||
				this.focus();
 | 
							focus();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		renoteDirectly() {
 | 
					function react(viaKeyboard = false): void {
 | 
				
			||||||
			os.apiWithDialog('notes/create', {
 | 
					 | 
				
			||||||
				renoteId: this.appearNote.id
 | 
					 | 
				
			||||||
			}, undefined, (res: any) => {
 | 
					 | 
				
			||||||
				os.alert({
 | 
					 | 
				
			||||||
					type: 'success',
 | 
					 | 
				
			||||||
					text: this.$ts.renoted,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}, (e: Error) => {
 | 
					 | 
				
			||||||
				if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.cantRenote,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				} else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.cantReRenote,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		react(viaKeyboard = false) {
 | 
					 | 
				
			||||||
	pleaseLogin();
 | 
						pleaseLogin();
 | 
				
			||||||
			this.blur();
 | 
						blur();
 | 
				
			||||||
			reactionPicker.show(this.$refs.reactButton, reaction => {
 | 
						reactionPicker.show(reactButton.value, reaction => {
 | 
				
			||||||
		os.api('notes/reactions/create', {
 | 
							os.api('notes/reactions/create', {
 | 
				
			||||||
					noteId: this.appearNote.id,
 | 
								noteId: appearNote.id,
 | 
				
			||||||
			reaction: reaction
 | 
								reaction: reaction
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}, () => {
 | 
						}, () => {
 | 
				
			||||||
				this.focus();
 | 
							focus();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		reactDirectly(reaction) {
 | 
					function undoReact(note): void {
 | 
				
			||||||
			os.api('notes/reactions/create', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
				reaction: reaction
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		undoReact(note) {
 | 
					 | 
				
			||||||
	const oldReaction = note.myReaction;
 | 
						const oldReaction = note.myReaction;
 | 
				
			||||||
	if (!oldReaction) return;
 | 
						if (!oldReaction) return;
 | 
				
			||||||
	os.api('notes/reactions/delete', {
 | 
						os.api('notes/reactions/delete', {
 | 
				
			||||||
		noteId: note.id
 | 
							noteId: note.id
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		favorite() {
 | 
					function onContextmenu(e): void {
 | 
				
			||||||
			pleaseLogin();
 | 
					 | 
				
			||||||
			os.apiWithDialog('notes/favorites/create', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			}, undefined, (res: any) => {
 | 
					 | 
				
			||||||
				os.alert({
 | 
					 | 
				
			||||||
					type: 'success',
 | 
					 | 
				
			||||||
					text: this.$ts.favorited,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}, (e: Error) => {
 | 
					 | 
				
			||||||
				if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.alreadyFavorited,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				} else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
 | 
					 | 
				
			||||||
					os.alert({
 | 
					 | 
				
			||||||
						type: 'error',
 | 
					 | 
				
			||||||
						text: this.$ts.cantFavorite,
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		del() {
 | 
					 | 
				
			||||||
			os.confirm({
 | 
					 | 
				
			||||||
				type: 'warning',
 | 
					 | 
				
			||||||
				text: this.$ts.noteDeleteConfirm,
 | 
					 | 
				
			||||||
			}).then(({ canceled }) => {
 | 
					 | 
				
			||||||
				if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				os.api('notes/delete', {
 | 
					 | 
				
			||||||
					noteId: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		delEdit() {
 | 
					 | 
				
			||||||
			os.confirm({
 | 
					 | 
				
			||||||
				type: 'warning',
 | 
					 | 
				
			||||||
				text: this.$ts.deleteAndEditConfirm,
 | 
					 | 
				
			||||||
			}).then(({ canceled }) => {
 | 
					 | 
				
			||||||
				if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				os.api('notes/delete', {
 | 
					 | 
				
			||||||
					noteId: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toggleFavorite(favorite: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toggleWatch(watch: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		toggleThreadMute(mute: boolean) {
 | 
					 | 
				
			||||||
			os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		getMenu() {
 | 
					 | 
				
			||||||
			let menu;
 | 
					 | 
				
			||||||
			if (this.$i) {
 | 
					 | 
				
			||||||
				const statePromise = os.api('notes/state', {
 | 
					 | 
				
			||||||
					noteId: this.appearNote.id
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				menu = [{
 | 
					 | 
				
			||||||
					icon: 'fas fa-copy',
 | 
					 | 
				
			||||||
					text: this.$ts.copyContent,
 | 
					 | 
				
			||||||
					action: this.copyContent
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					icon: 'fas fa-link',
 | 
					 | 
				
			||||||
					text: this.$ts.copyLink,
 | 
					 | 
				
			||||||
					action: this.copyLink
 | 
					 | 
				
			||||||
				}, (this.appearNote.url || this.appearNote.uri) ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-external-link-square-alt',
 | 
					 | 
				
			||||||
					text: this.$ts.showOnRemote,
 | 
					 | 
				
			||||||
					action: () => {
 | 
					 | 
				
			||||||
						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} : undefined,
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					icon: 'fas fa-share-alt',
 | 
					 | 
				
			||||||
					text: this.$ts.share,
 | 
					 | 
				
			||||||
					action: this.share
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				this.$instance.translatorAvailable ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-language',
 | 
					 | 
				
			||||||
					text: this.$ts.translate,
 | 
					 | 
				
			||||||
					action: this.translate
 | 
					 | 
				
			||||||
				} : undefined,
 | 
					 | 
				
			||||||
				null,
 | 
					 | 
				
			||||||
				statePromise.then(state => state.isFavorited ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-star',
 | 
					 | 
				
			||||||
					text: this.$ts.unfavorite,
 | 
					 | 
				
			||||||
					action: () => this.toggleFavorite(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-star',
 | 
					 | 
				
			||||||
					text: this.$ts.favorite,
 | 
					 | 
				
			||||||
					action: () => this.toggleFavorite(true)
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					icon: 'fas fa-paperclip',
 | 
					 | 
				
			||||||
					text: this.$ts.clip,
 | 
					 | 
				
			||||||
					action: () => this.clip()
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				(this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-eye-slash',
 | 
					 | 
				
			||||||
					text: this.$ts.unwatch,
 | 
					 | 
				
			||||||
					action: () => this.toggleWatch(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-eye',
 | 
					 | 
				
			||||||
					text: this.$ts.watch,
 | 
					 | 
				
			||||||
					action: () => this.toggleWatch(true)
 | 
					 | 
				
			||||||
				}) : undefined,
 | 
					 | 
				
			||||||
				statePromise.then(state => state.isMutedThread ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-comment-slash',
 | 
					 | 
				
			||||||
					text: this.$ts.unmuteThread,
 | 
					 | 
				
			||||||
					action: () => this.toggleThreadMute(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-comment-slash',
 | 
					 | 
				
			||||||
					text: this.$ts.muteThread,
 | 
					 | 
				
			||||||
					action: () => this.toggleThreadMute(true)
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
				this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-thumbtack',
 | 
					 | 
				
			||||||
					text: this.$ts.unpin,
 | 
					 | 
				
			||||||
					action: () => this.togglePin(false)
 | 
					 | 
				
			||||||
				} : {
 | 
					 | 
				
			||||||
					icon: 'fas fa-thumbtack',
 | 
					 | 
				
			||||||
					text: this.$ts.pin,
 | 
					 | 
				
			||||||
					action: () => this.togglePin(true)
 | 
					 | 
				
			||||||
				} : undefined,
 | 
					 | 
				
			||||||
				/*
 | 
					 | 
				
			||||||
				...(this.$i.isModerator || this.$i.isAdmin ? [
 | 
					 | 
				
			||||||
					null,
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						icon: 'fas fa-bullhorn',
 | 
					 | 
				
			||||||
						text: this.$ts.promote,
 | 
					 | 
				
			||||||
						action: this.promote
 | 
					 | 
				
			||||||
					}]
 | 
					 | 
				
			||||||
					: []
 | 
					 | 
				
			||||||
				),*/
 | 
					 | 
				
			||||||
				...(this.appearNote.userId != this.$i.id ? [
 | 
					 | 
				
			||||||
					null,
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						icon: 'fas fa-exclamation-circle',
 | 
					 | 
				
			||||||
						text: this.$ts.reportAbuse,
 | 
					 | 
				
			||||||
						action: () => {
 | 
					 | 
				
			||||||
							const u = `${url}/notes/${this.appearNote.id}`;
 | 
					 | 
				
			||||||
							os.popup(import('@/components/abuse-report-window.vue'), {
 | 
					 | 
				
			||||||
								user: this.appearNote.user,
 | 
					 | 
				
			||||||
								initialComment: `Note: ${u}\n-----\n`
 | 
					 | 
				
			||||||
							}, {}, 'closed');
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}]
 | 
					 | 
				
			||||||
					: []
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
				...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
 | 
					 | 
				
			||||||
					null,
 | 
					 | 
				
			||||||
					this.appearNote.userId == this.$i.id ? {
 | 
					 | 
				
			||||||
						icon: 'fas fa-edit',
 | 
					 | 
				
			||||||
						text: this.$ts.deleteAndEdit,
 | 
					 | 
				
			||||||
						action: this.delEdit
 | 
					 | 
				
			||||||
					} : undefined,
 | 
					 | 
				
			||||||
					{
 | 
					 | 
				
			||||||
						icon: 'fas fa-trash-alt',
 | 
					 | 
				
			||||||
						text: this.$ts.delete,
 | 
					 | 
				
			||||||
						danger: true,
 | 
					 | 
				
			||||||
						action: this.del
 | 
					 | 
				
			||||||
					}]
 | 
					 | 
				
			||||||
					: []
 | 
					 | 
				
			||||||
				)]
 | 
					 | 
				
			||||||
				.filter(x => x !== undefined);
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				menu = [{
 | 
					 | 
				
			||||||
					icon: 'fas fa-copy',
 | 
					 | 
				
			||||||
					text: this.$ts.copyContent,
 | 
					 | 
				
			||||||
					action: this.copyContent
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					icon: 'fas fa-link',
 | 
					 | 
				
			||||||
					text: this.$ts.copyLink,
 | 
					 | 
				
			||||||
					action: this.copyLink
 | 
					 | 
				
			||||||
				}, (this.appearNote.url || this.appearNote.uri) ? {
 | 
					 | 
				
			||||||
					icon: 'fas fa-external-link-square-alt',
 | 
					 | 
				
			||||||
					text: this.$ts.showOnRemote,
 | 
					 | 
				
			||||||
					action: () => {
 | 
					 | 
				
			||||||
						window.open(this.appearNote.url || this.appearNote.uri, '_blank');
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} : undefined]
 | 
					 | 
				
			||||||
				.filter(x => x !== undefined);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (noteActions.length > 0) {
 | 
					 | 
				
			||||||
				menu = menu.concat([null, ...noteActions.map(action => ({
 | 
					 | 
				
			||||||
					icon: 'fas fa-plug',
 | 
					 | 
				
			||||||
					text: action.title,
 | 
					 | 
				
			||||||
					action: () => {
 | 
					 | 
				
			||||||
						action.handler(this.appearNote);
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}))]);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return menu;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onContextmenu(e) {
 | 
					 | 
				
			||||||
	const isLink = (el: HTMLElement) => {
 | 
						const isLink = (el: HTMLElement) => {
 | 
				
			||||||
		if (el.tagName === 'A') return true;
 | 
							if (el.tagName === 'A') return true;
 | 
				
			||||||
		if (el.parentElement) {
 | 
							if (el.parentElement) {
 | 
				
			||||||
| 
						 | 
					@ -707,153 +221,59 @@ export default defineComponent({
 | 
				
			||||||
	if (isLink(e.target)) return;
 | 
						if (isLink(e.target)) return;
 | 
				
			||||||
	if (window.getSelection().toString() !== '') return;
 | 
						if (window.getSelection().toString() !== '') return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (this.$store.state.useReactionPickerForContextMenu) {
 | 
						if (defaultStore.state.useReactionPickerForContextMenu) {
 | 
				
			||||||
		e.preventDefault();
 | 
							e.preventDefault();
 | 
				
			||||||
				this.react();
 | 
							react();
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
				os.contextMenu(this.getMenu(), e).then(this.focus);
 | 
							os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), e).then(focus);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		menu(viaKeyboard = false) {
 | 
					function menu(viaKeyboard = false): void {
 | 
				
			||||||
			os.popupMenu(this.getMenu(), this.$refs.menuButton, {
 | 
						os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
 | 
				
			||||||
		viaKeyboard
 | 
							viaKeyboard
 | 
				
			||||||
			}).then(this.focus);
 | 
						}).then(focus);
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		showRenoteMenu(viaKeyboard = false) {
 | 
					function showRenoteMenu(viaKeyboard = false): void {
 | 
				
			||||||
			if (!this.isMyRenote) return;
 | 
						if (!isMyRenote) return;
 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
				text: this.$ts.unrenote,
 | 
							text: i18n.locale.unrenote,
 | 
				
			||||||
		icon: 'fas fa-trash-alt',
 | 
							icon: 'fas fa-trash-alt',
 | 
				
			||||||
		danger: true,
 | 
							danger: true,
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
			os.api('notes/delete', {
 | 
								os.api('notes/delete', {
 | 
				
			||||||
						noteId: this.note.id
 | 
									noteId: props.note.id
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
					this.isDeleted = true;
 | 
								isDeleted.value = true;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
			}], this.$refs.renoteTime, {
 | 
						}], renoteTime.value, {
 | 
				
			||||||
		viaKeyboard: viaKeyboard
 | 
							viaKeyboard: viaKeyboard
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		toggleShowContent() {
 | 
					function focus() {
 | 
				
			||||||
			this.showContent = !this.showContent;
 | 
						el.value.focus();
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		copyContent() {
 | 
					function blur() {
 | 
				
			||||||
			copyToClipboard(this.appearNote.text);
 | 
						el.value.blur();
 | 
				
			||||||
			os.success();
 | 
					}
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		copyLink() {
 | 
					function focusBefore() {
 | 
				
			||||||
			copyToClipboard(`${url}/notes/${this.appearNote.id}`);
 | 
						focusPrev(el.value);
 | 
				
			||||||
			os.success();
 | 
					}
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		togglePin(pin: boolean) {
 | 
					function focusAfter() {
 | 
				
			||||||
			os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
 | 
						focusNext(el.value);
 | 
				
			||||||
				noteId: this.appearNote.id
 | 
					}
 | 
				
			||||||
			}, undefined, null, e => {
 | 
					
 | 
				
			||||||
				if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
 | 
					function readPromo() {
 | 
				
			||||||
					os.alert({
 | 
						os.api('promo/read', {
 | 
				
			||||||
						type: 'error',
 | 
							noteId: appearNote.id
 | 
				
			||||||
						text: this.$ts.pinLimitExceeded
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
				}
 | 
						isDeleted.value = true;
 | 
				
			||||||
			});
 | 
					}
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		async clip() {
 | 
					 | 
				
			||||||
			const clips = await os.api('clips/list');
 | 
					 | 
				
			||||||
			os.popupMenu([{
 | 
					 | 
				
			||||||
				icon: 'fas fa-plus',
 | 
					 | 
				
			||||||
				text: this.$ts.createNew,
 | 
					 | 
				
			||||||
				action: async () => {
 | 
					 | 
				
			||||||
					const { canceled, result } = await os.form(this.$ts.createNewClip, {
 | 
					 | 
				
			||||||
						name: {
 | 
					 | 
				
			||||||
							type: 'string',
 | 
					 | 
				
			||||||
							label: this.$ts.name
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						description: {
 | 
					 | 
				
			||||||
							type: 'string',
 | 
					 | 
				
			||||||
							required: false,
 | 
					 | 
				
			||||||
							multiline: true,
 | 
					 | 
				
			||||||
							label: this.$ts.description
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						isPublic: {
 | 
					 | 
				
			||||||
							type: 'boolean',
 | 
					 | 
				
			||||||
							label: this.$ts.public,
 | 
					 | 
				
			||||||
							default: false
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
					if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					const clip = await os.apiWithDialog('clips/create', result);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}, null, ...clips.map(clip => ({
 | 
					 | 
				
			||||||
				text: clip.name,
 | 
					 | 
				
			||||||
				action: () => {
 | 
					 | 
				
			||||||
					os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}))], this.$refs.menuButton, {
 | 
					 | 
				
			||||||
			}).then(this.focus);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		async promote() {
 | 
					 | 
				
			||||||
			const { canceled, result: days } = await os.inputNumber({
 | 
					 | 
				
			||||||
				title: this.$ts.numberOfDays,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (canceled) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			os.apiWithDialog('admin/promo/create', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
				expiresAt: Date.now() + (86400000 * days)
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		share() {
 | 
					 | 
				
			||||||
			navigator.share({
 | 
					 | 
				
			||||||
				title: this.$t('noteOf', { user: this.appearNote.user.name }),
 | 
					 | 
				
			||||||
				text: this.appearNote.text,
 | 
					 | 
				
			||||||
				url: `${url}/notes/${this.appearNote.id}`
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		async translate() {
 | 
					 | 
				
			||||||
			if (this.translation != null) return;
 | 
					 | 
				
			||||||
			this.translating = true;
 | 
					 | 
				
			||||||
			const res = await os.api('notes/translate', {
 | 
					 | 
				
			||||||
				noteId: this.appearNote.id,
 | 
					 | 
				
			||||||
				targetLang: localStorage.getItem('lang') || navigator.language,
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
			this.translating = false;
 | 
					 | 
				
			||||||
			this.translation = res;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		focus() {
 | 
					 | 
				
			||||||
			this.$el.focus();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		blur() {
 | 
					 | 
				
			||||||
			this.$el.blur();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		focusBefore() {
 | 
					 | 
				
			||||||
			focusPrev(this.$el);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		focusAfter() {
 | 
					 | 
				
			||||||
			focusNext(this.$el);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		userPage
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@
 | 
				
			||||||
	<template #default="{ items: notes }">
 | 
						<template #default="{ items: notes }">
 | 
				
			||||||
		<div class="giivymft" :class="{ noGap }">
 | 
							<div class="giivymft" :class="{ noGap }">
 | 
				
			||||||
			<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
 | 
								<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
 | 
				
			||||||
				<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/>
 | 
									<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
 | 
				
			||||||
			</XList>
 | 
								</XList>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
| 
						 | 
					@ -31,10 +31,6 @@ const props = defineProps<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 | 
					const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const updated = (oldValue, newValue) => {
 | 
					 | 
				
			||||||
	pagingComponent.value?.updateItem(oldValue.id, () => newValue);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
defineExpose({
 | 
					defineExpose({
 | 
				
			||||||
	prepend: (note) => {
 | 
						prepend: (note) => {
 | 
				
			||||||
		pagingComponent.value?.prepend(note);
 | 
							pagingComponent.value?.prepend(note);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,7 +29,7 @@ export default defineComponent({
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
		setTimeout(() => {
 | 
							window.setTimeout(() => {
 | 
				
			||||||
			this.showing = false;
 | 
								this.showing = false;
 | 
				
			||||||
		}, 6000);
 | 
							}, 6000);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<template #default="{ items: notifications }">
 | 
						<template #default="{ items: notifications }">
 | 
				
			||||||
		<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
 | 
							<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
 | 
				
			||||||
			<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification, $event)"/>
 | 
								<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
 | 
				
			||||||
			<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
 | 
								<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
 | 
				
			||||||
		</XList>
 | 
							</XList>
 | 
				
			||||||
	</template>
 | 
						</template>
 | 
				
			||||||
| 
						 | 
					@ -62,13 +62,6 @@ const onNotification = (notification) => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const noteUpdated = (item, note) => {
 | 
					 | 
				
			||||||
	pagingComponent.value?.updateItem(item.id, old => ({
 | 
					 | 
				
			||||||
		...old,
 | 
					 | 
				
			||||||
		note: note,
 | 
					 | 
				
			||||||
	}));
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
	const connection = stream.useChannel('main');
 | 
						const connection = stream.useChannel('main');
 | 
				
			||||||
	connection.on('notification', onNotification);
 | 
						connection.on('notification', onNotification);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -1,25 +1,13 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkEmoji :emoji="reaction" :custom-emojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
 | 
					<MkEmoji :emoji="reaction" :custom-emojis="customEmojis || []" :is-reaction="true" :normal="true" :no-style="noStyle"/>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	props: {
 | 
						reaction: string;
 | 
				
			||||||
		reaction: {
 | 
						customEmojis?: any[]; // TODO
 | 
				
			||||||
			type: String,
 | 
						noStyle?: boolean;
 | 
				
			||||||
			required: true
 | 
					}>();
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		customEmojis: {
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: () => []
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		noStyle: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')">
 | 
					<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
 | 
				
			||||||
	<div class="beeadbfb">
 | 
						<div class="beeadbfb">
 | 
				
			||||||
		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
							<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
				
			||||||
		<div class="name">{{ reaction.replace('@.', '') }}</div>
 | 
							<div class="name">{{ reaction.replace('@.', '') }}</div>
 | 
				
			||||||
| 
						 | 
					@ -7,31 +7,20 @@
 | 
				
			||||||
</MkTooltip>
 | 
					</MkTooltip>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import MkTooltip from './ui/tooltip.vue';
 | 
					import MkTooltip from './ui/tooltip.vue';
 | 
				
			||||||
import XReactionIcon from './reaction-icon.vue';
 | 
					import XReactionIcon from './reaction-icon.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						reaction: string;
 | 
				
			||||||
		MkTooltip,
 | 
						emojis: any[]; // TODO
 | 
				
			||||||
		XReactionIcon,
 | 
						source: any; // TODO
 | 
				
			||||||
	},
 | 
					}>();
 | 
				
			||||||
	props: {
 | 
					
 | 
				
			||||||
		reaction: {
 | 
					const emit = defineEmits<{
 | 
				
			||||||
			type: String,
 | 
						(e: 'closed'): void;
 | 
				
			||||||
			required: true,
 | 
					}>();
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		emojis: {
 | 
					 | 
				
			||||||
			type: Array,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		source: {
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	emits: ['closed'],
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')">
 | 
					<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
 | 
				
			||||||
	<div class="bqxuuuey">
 | 
						<div class="bqxuuuey">
 | 
				
			||||||
		<div class="reaction">
 | 
							<div class="reaction">
 | 
				
			||||||
			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
								<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
 | 
				
			||||||
| 
						 | 
					@ -16,39 +16,22 @@
 | 
				
			||||||
</MkTooltip>
 | 
					</MkTooltip>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import MkTooltip from './ui/tooltip.vue';
 | 
					import MkTooltip from './ui/tooltip.vue';
 | 
				
			||||||
import XReactionIcon from './reaction-icon.vue';
 | 
					import XReactionIcon from './reaction-icon.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						reaction: string;
 | 
				
			||||||
		MkTooltip,
 | 
						users: any[]; // TODO
 | 
				
			||||||
		XReactionIcon
 | 
						count: number;
 | 
				
			||||||
	},
 | 
						emojis: any[]; // TODO
 | 
				
			||||||
	props: {
 | 
						source: any; // TODO
 | 
				
			||||||
		reaction: {
 | 
					}>();
 | 
				
			||||||
			type: String,
 | 
					
 | 
				
			||||||
			required: true,
 | 
					const emit = defineEmits<{
 | 
				
			||||||
		},
 | 
						(e: 'closed'): void;
 | 
				
			||||||
		users: {
 | 
					}>();
 | 
				
			||||||
			type: Array,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		count: {
 | 
					 | 
				
			||||||
			type: Number,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		emojis: {
 | 
					 | 
				
			||||||
			type: Array,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		source: {
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	emits: ['closed'],
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,31 +4,19 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { computed } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
 | 
					import { $i } from '@/account';
 | 
				
			||||||
import XReaction from './reactions-viewer.reaction.vue';
 | 
					import XReaction from './reactions-viewer.reaction.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
		XReaction
 | 
					}>();
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
	props: {
 | 
					const initialReactions = new Set(Object.keys(props.note.reactions));
 | 
				
			||||||
		note: {
 | 
					
 | 
				
			||||||
			type: Object,
 | 
					const isMe = computed(() => $i && $i.id === props.note.userId);
 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			initialReactions: new Set(Object.keys(this.note.reactions))
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	computed: {
 | 
					 | 
				
			||||||
		isMe(): boolean {
 | 
					 | 
				
			||||||
			return this.$i && this.$i.id === this.note.userId;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="$emit('closed')">
 | 
					<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')">
 | 
				
			||||||
	<div class="beaffaef">
 | 
						<div class="beaffaef">
 | 
				
			||||||
		<div v-for="u in users" :key="u.id" class="user">
 | 
							<div v-for="u in users" :key="u.id" class="user">
 | 
				
			||||||
			<MkAvatar class="avatar" :user="u"/>
 | 
								<MkAvatar class="avatar" :user="u"/>
 | 
				
			||||||
| 
						 | 
					@ -10,29 +10,19 @@
 | 
				
			||||||
</MkTooltip>
 | 
					</MkTooltip>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import MkTooltip from './ui/tooltip.vue';
 | 
					import MkTooltip from './ui/tooltip.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						users: any[]; // TODO
 | 
				
			||||||
		MkTooltip,
 | 
						count: number;
 | 
				
			||||||
	},
 | 
						source: any; // TODO
 | 
				
			||||||
	props: {
 | 
					}>();
 | 
				
			||||||
		users: {
 | 
					
 | 
				
			||||||
			type: Array,
 | 
					const emit = defineEmits<{
 | 
				
			||||||
			required: true,
 | 
						(e: 'closed'): void;
 | 
				
			||||||
		},
 | 
					}>();
 | 
				
			||||||
		count: {
 | 
					 | 
				
			||||||
			type: Number,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		source: {
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	emits: ['closed'],
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,7 +94,7 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		onMounted(() => {
 | 
							onMounted(() => {
 | 
				
			||||||
			setTimeout(() => {
 | 
								window.setTimeout(() => {
 | 
				
			||||||
				context.emit('end');
 | 
									context.emit('end');
 | 
				
			||||||
			}, 1100);
 | 
								}, 1100);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,8 +2,8 @@
 | 
				
			||||||
<XModalWindow ref="dialog"
 | 
					<XModalWindow ref="dialog"
 | 
				
			||||||
	:width="370"
 | 
						:width="370"
 | 
				
			||||||
	:height="400"
 | 
						:height="400"
 | 
				
			||||||
	@close="$refs.dialog.close()"
 | 
						@close="dialog.close()"
 | 
				
			||||||
	@closed="$emit('closed')"
 | 
						@closed="emit('closed')"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<template #header>{{ $ts.login }}</template>
 | 
						<template #header>{{ $ts.login }}</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,32 +11,26 @@
 | 
				
			||||||
</XModalWindow>
 | 
					</XModalWindow>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
					import XModalWindow from '@/components/ui/modal-window.vue';
 | 
				
			||||||
import MkSignin from './signin.vue';
 | 
					import MkSignin from './signin.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	components: {
 | 
						autoSet?: boolean;
 | 
				
			||||||
		MkSignin,
 | 
					}>(), {
 | 
				
			||||||
		XModalWindow,
 | 
						autoSet: false,
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		autoSet: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: false,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	emits: ['done', 'closed'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		onLogin(res) {
 | 
					 | 
				
			||||||
			this.$emit('done', res);
 | 
					 | 
				
			||||||
			this.$refs.dialog.close();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'done'): void;
 | 
				
			||||||
 | 
						(e: 'closed'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dialog = $ref<InstanceType<typeof XModalWindow>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onLogin(res) {
 | 
				
			||||||
 | 
						emit('done', res);
 | 
				
			||||||
 | 
						dialog.close();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
<XModalWindow ref="dialog"
 | 
					<XModalWindow ref="dialog"
 | 
				
			||||||
	:width="366"
 | 
						:width="366"
 | 
				
			||||||
	:height="500"
 | 
						:height="500"
 | 
				
			||||||
	@close="$refs.dialog.close()"
 | 
						@close="dialog.close()"
 | 
				
			||||||
	@closed="$emit('closed')"
 | 
						@closed="$emit('closed')"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<template #header>{{ $ts.signup }}</template>
 | 
						<template #header>{{ $ts.signup }}</template>
 | 
				
			||||||
| 
						 | 
					@ -15,36 +15,30 @@
 | 
				
			||||||
</XModalWindow>
 | 
					</XModalWindow>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import XModalWindow from '@/components/ui/modal-window.vue';
 | 
					import XModalWindow from '@/components/ui/modal-window.vue';
 | 
				
			||||||
import XSignup from './signup.vue';
 | 
					import XSignup from './signup.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	components: {
 | 
						autoSet?: boolean;
 | 
				
			||||||
		XSignup,
 | 
					}>(), {
 | 
				
			||||||
		XModalWindow,
 | 
						autoSet: false,
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		autoSet: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
			default: false,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	emits: ['done', 'closed'],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		onSignup(res) {
 | 
					 | 
				
			||||||
			this.$emit('done', res);
 | 
					 | 
				
			||||||
			this.$refs.dialog.close();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		onSignupEmailPending() {
 | 
					 | 
				
			||||||
			this.$refs.dialog.close();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'done'): void;
 | 
				
			||||||
 | 
						(e: 'closed'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dialog = $ref<InstanceType<typeof XModalWindow>>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onSignup(res) {
 | 
				
			||||||
 | 
						emit('done', res);
 | 
				
			||||||
 | 
						dialog.close();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onSignupEmailPending() {
 | 
				
			||||||
 | 
						dialog.close();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,35 +21,21 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import XPoll from './poll.vue';
 | 
					import XPoll from './poll.vue';
 | 
				
			||||||
import XMediaList from './media-list.vue';
 | 
					import XMediaList from './media-list.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	components: {
 | 
						note: misskey.entities.Note;
 | 
				
			||||||
		XPoll,
 | 
					}>();
 | 
				
			||||||
		XMediaList,
 | 
					
 | 
				
			||||||
	},
 | 
					const collapsed = $ref(
 | 
				
			||||||
	props: {
 | 
						props.note.cw == null && props.note.text != null && (
 | 
				
			||||||
		note: {
 | 
							(props.note.text.split('\n').length > 9) ||
 | 
				
			||||||
			type: Object,
 | 
							(props.note.text.length > 500)
 | 
				
			||||||
			required: true
 | 
						));
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			collapsed: false,
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		this.collapsed = this.note.cw == null && this.note.text && (
 | 
					 | 
				
			||||||
			(this.note.text.split('\n').length > 9) ||
 | 
					 | 
				
			||||||
			(this.note.text.length > 500)
 | 
					 | 
				
			||||||
		);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ const showing = ref(true);
 | 
				
			||||||
const zIndex = os.claimZIndex('high');
 | 
					const zIndex = os.claimZIndex('high');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
	setTimeout(() => {
 | 
						window.setTimeout(() => {
 | 
				
			||||||
		showing.value = false;
 | 
							showing.value = false;
 | 
				
			||||||
	}, 4000);
 | 
						}, 4000);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,14 +117,14 @@ export default defineComponent({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
 | 
								const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			setTimeout(() => {
 | 
								window.setTimeout(() => {
 | 
				
			||||||
				ripple.style.transform = 'scale(' + (scale / 2) + ')';
 | 
									ripple.style.transform = 'scale(' + (scale / 2) + ')';
 | 
				
			||||||
			}, 1);
 | 
								}, 1);
 | 
				
			||||||
			setTimeout(() => {
 | 
								window.setTimeout(() => {
 | 
				
			||||||
				ripple.style.transition = 'all 1s ease';
 | 
									ripple.style.transition = 'all 1s ease';
 | 
				
			||||||
				ripple.style.opacity = '0';
 | 
									ripple.style.opacity = '0';
 | 
				
			||||||
			}, 1000);
 | 
								}, 1000);
 | 
				
			||||||
			setTimeout(() => {
 | 
								window.setTimeout(() => {
 | 
				
			||||||
				if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
 | 
									if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
 | 
				
			||||||
			}, 2000);
 | 
								}, 2000);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -211,7 +211,7 @@ export default defineComponent({
 | 
				
			||||||
				contentClicking = true;
 | 
									contentClicking = true;
 | 
				
			||||||
				window.addEventListener('mouseup', e => {
 | 
									window.addEventListener('mouseup', e => {
 | 
				
			||||||
					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
 | 
										// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
 | 
				
			||||||
					setTimeout(() => {
 | 
										window.setTimeout(() => {
 | 
				
			||||||
						contentClicking = false;
 | 
											contentClicking = false;
 | 
				
			||||||
					}, 100);
 | 
										}, 100);
 | 
				
			||||||
				}, { passive: true, once: true });
 | 
									}, { passive: true, once: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,6 @@ const init = async (): Promise<void> => {
 | 
				
			||||||
	}).then(res => {
 | 
						}).then(res => {
 | 
				
			||||||
		for (let i = 0; i < res.length; i++) {
 | 
							for (let i = 0; i < res.length; i++) {
 | 
				
			||||||
			const item = res[i];
 | 
								const item = res[i];
 | 
				
			||||||
			markRaw(item);
 | 
					 | 
				
			||||||
			if (props.pagination.reversed) {
 | 
								if (props.pagination.reversed) {
 | 
				
			||||||
				if (i === res.length - 2) item._shouldInsertAd_ = true;
 | 
									if (i === res.length - 2) item._shouldInsertAd_ = true;
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
| 
						 | 
					@ -134,7 +133,6 @@ const fetchMore = async (): Promise<void> => {
 | 
				
			||||||
	}).then(res => {
 | 
						}).then(res => {
 | 
				
			||||||
		for (let i = 0; i < res.length; i++) {
 | 
							for (let i = 0; i < res.length; i++) {
 | 
				
			||||||
			const item = res[i];
 | 
								const item = res[i];
 | 
				
			||||||
			markRaw(item);
 | 
					 | 
				
			||||||
			if (props.pagination.reversed) {
 | 
								if (props.pagination.reversed) {
 | 
				
			||||||
				if (i === res.length - 9) item._shouldInsertAd_ = true;
 | 
									if (i === res.length - 9) item._shouldInsertAd_ = true;
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
| 
						 | 
					@ -169,9 +167,6 @@ const fetchMoreAhead = async (): Promise<void> => {
 | 
				
			||||||
			sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
 | 
								sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
	}).then(res => {
 | 
						}).then(res => {
 | 
				
			||||||
		for (const item of res) {
 | 
					 | 
				
			||||||
			markRaw(item);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if (res.length > SECOND_FETCH_LIMIT) {
 | 
							if (res.length > SECOND_FETCH_LIMIT) {
 | 
				
			||||||
			res.pop();
 | 
								res.pop();
 | 
				
			||||||
			items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
 | 
								items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
	<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
 | 
						<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
 | 
					<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
 | 
				
			||||||
	<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
 | 
						<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
 | 
					<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
 | 
				
			||||||
	<transition name="zoom" mode="out-in">
 | 
						<transition name="zoom" mode="out-in">
 | 
				
			||||||
| 
						 | 
					@ -32,110 +32,80 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { onMounted, onUnmounted } from 'vue';
 | 
				
			||||||
import { url as local, lang } from '@/config';
 | 
					import { url as local, lang } from '@/config';
 | 
				
			||||||
import * as os from '@/os';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	props: {
 | 
						url: string;
 | 
				
			||||||
		url: {
 | 
						detail?: boolean;
 | 
				
			||||||
			type: String,
 | 
						compact?: boolean;
 | 
				
			||||||
			require: true
 | 
					}>(), {
 | 
				
			||||||
		},
 | 
						detail: false,
 | 
				
			||||||
 | 
						compact: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		detail: {
 | 
					const self = props.url.startsWith(local);
 | 
				
			||||||
			type: Boolean,
 | 
					const attr = self ? 'to' : 'href';
 | 
				
			||||||
			required: false,
 | 
					const target = self ? null : '_blank';
 | 
				
			||||||
			default: false
 | 
					let fetching = $ref(true);
 | 
				
			||||||
		},
 | 
					let title = $ref<string | null>(null);
 | 
				
			||||||
 | 
					let description = $ref<string | null>(null);
 | 
				
			||||||
		compact: {
 | 
					let thumbnail = $ref<string | null>(null);
 | 
				
			||||||
			type: Boolean,
 | 
					let icon = $ref<string | null>(null);
 | 
				
			||||||
			required: false,
 | 
					let sitename = $ref<string | null>(null);
 | 
				
			||||||
			default: false
 | 
					let player = $ref({
 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		const self = this.url.startsWith(local);
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			local,
 | 
					 | 
				
			||||||
			fetching: true,
 | 
					 | 
				
			||||||
			title: null,
 | 
					 | 
				
			||||||
			description: null,
 | 
					 | 
				
			||||||
			thumbnail: null,
 | 
					 | 
				
			||||||
			icon: null,
 | 
					 | 
				
			||||||
			sitename: null,
 | 
					 | 
				
			||||||
			player: {
 | 
					 | 
				
			||||||
	url: null,
 | 
						url: null,
 | 
				
			||||||
	width: null,
 | 
						width: null,
 | 
				
			||||||
	height: null
 | 
						height: null
 | 
				
			||||||
			},
 | 
					});
 | 
				
			||||||
			tweetId: null,
 | 
					let playerEnabled = $ref(false);
 | 
				
			||||||
			tweetExpanded: this.detail,
 | 
					let tweetId = $ref<string | null>(null);
 | 
				
			||||||
			embedId: `embed${Math.random().toString().replace(/\D/,'')}`,
 | 
					let tweetExpanded = $ref(props.detail);
 | 
				
			||||||
			tweetHeight: 150,
 | 
					const embedId = `embed${Math.random().toString().replace(/\D/,'')}`;
 | 
				
			||||||
			tweetLeft: 0,
 | 
					let tweetHeight = $ref(150);
 | 
				
			||||||
			playerEnabled: false,
 | 
					 | 
				
			||||||
			self: self,
 | 
					 | 
				
			||||||
			attr: self ? 'to' : 'href',
 | 
					 | 
				
			||||||
			target: self ? null : '_blank',
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	created() {
 | 
					const requestUrl = new URL(props.url);
 | 
				
			||||||
		const requestUrl = new URL(this.url);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (requestUrl.hostname == 'twitter.com') {
 | 
					if (requestUrl.hostname == 'twitter.com') {
 | 
				
			||||||
	const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
 | 
						const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
 | 
				
			||||||
			if (m) this.tweetId = m[1];
 | 
						if (m) tweetId = m[1];
 | 
				
			||||||
		}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
 | 
					if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
 | 
				
			||||||
	requestUrl.hostname = 'www.youtube.com';
 | 
						requestUrl.hostname = 'www.youtube.com';
 | 
				
			||||||
		}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
 | 
					const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		requestUrl.hash = '';
 | 
					requestUrl.hash = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
 | 
					fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
 | 
				
			||||||
	res.json().then(info => {
 | 
						res.json().then(info => {
 | 
				
			||||||
		if (info.url == null) return;
 | 
							if (info.url == null) return;
 | 
				
			||||||
				this.title = info.title;
 | 
							title = info.title;
 | 
				
			||||||
				this.description = info.description;
 | 
							description = info.description;
 | 
				
			||||||
				this.thumbnail = info.thumbnail;
 | 
							thumbnail = info.thumbnail;
 | 
				
			||||||
				this.icon = info.icon;
 | 
							icon = info.icon;
 | 
				
			||||||
				this.sitename = info.sitename;
 | 
							sitename = info.sitename;
 | 
				
			||||||
				this.fetching = false;
 | 
							fetching = false;
 | 
				
			||||||
				this.player = info.player;
 | 
							player = info.player;
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
		});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		(window as any).addEventListener('message', this.adjustTweetHeight);
 | 
					function adjustTweetHeight(message: any) {
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		// 300pxないと絶対右にはみ出るので左に移動してしまう
 | 
					 | 
				
			||||||
		const areaWidth = (this.$el as any)?.clientWidth;
 | 
					 | 
				
			||||||
		if (areaWidth && areaWidth < 300) this.tweetLeft = areaWidth - 241;
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	beforeUnmount() {
 | 
					 | 
				
			||||||
		(window as any).removeEventListener('message', this.adjustTweetHeight);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		adjustTweetHeight(message: any) {
 | 
					 | 
				
			||||||
	if (message.origin !== 'https://platform.twitter.com') return;
 | 
						if (message.origin !== 'https://platform.twitter.com') return;
 | 
				
			||||||
	const embed = message.data?.['twttr.embed'];
 | 
						const embed = message.data?.['twttr.embed'];
 | 
				
			||||||
	if (embed?.method !== 'twttr.private.resize') return;
 | 
						if (embed?.method !== 'twttr.private.resize') return;
 | 
				
			||||||
			if (embed?.id !== this.embedId) return;
 | 
						if (embed?.id !== embedId) return;
 | 
				
			||||||
	const height = embed?.params[0]?.height;
 | 
						const height = embed?.params[0]?.height;
 | 
				
			||||||
			if (height) this.tweetHeight = height;
 | 
						if (height) tweetHeight = height;
 | 
				
			||||||
 		},
 | 
					}
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
 | 
					(window as any).addEventListener('message', adjustTweetHeight);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onUnmounted(() => {
 | 
				
			||||||
 | 
						(window as any).removeEventListener('message', adjustTweetHeight);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,26 +2,21 @@
 | 
				
			||||||
<div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div>
 | 
					<div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	props: {
 | 
						user: misskey.entities.User;
 | 
				
			||||||
		user: {
 | 
					}>();
 | 
				
			||||||
			type: Object,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	computed: {
 | 
					const text = $computed(() => {
 | 
				
			||||||
		text(): string {
 | 
						switch (props.user.onlineStatus) {
 | 
				
			||||||
			switch (this.user.onlineStatus) {
 | 
							case 'online': return i18n.locale.online;
 | 
				
			||||||
				case 'online': return this.$ts.online;
 | 
							case 'active': return i18n.locale.active;
 | 
				
			||||||
				case 'active': return this.$ts.active;
 | 
							case 'offline': return i18n.locale.offline;
 | 
				
			||||||
				case 'offline': return this.$ts.offline;
 | 
							case 'unknown': return i18n.locale.unknown;
 | 
				
			||||||
				case 'unknown': return this.$ts.unknown;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,28 +1,28 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
 | 
					<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
 | 
				
			||||||
	<div class="gqyayizv _popup">
 | 
						<div class="gqyayizv _popup">
 | 
				
			||||||
		<button key="public" class="_button" :class="{ active: v == 'public' }" data-index="1" @click="choose('public')">
 | 
							<button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
 | 
				
			||||||
			<div><i class="fas fa-globe"></i></div>
 | 
								<div><i class="fas fa-globe"></i></div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<span>{{ $ts._visibility.public }}</span>
 | 
									<span>{{ $ts._visibility.public }}</span>
 | 
				
			||||||
				<span>{{ $ts._visibility.publicDescription }}</span>
 | 
									<span>{{ $ts._visibility.publicDescription }}</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</button>
 | 
							</button>
 | 
				
			||||||
		<button key="home" class="_button" :class="{ active: v == 'home' }" data-index="2" @click="choose('home')">
 | 
							<button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
 | 
				
			||||||
			<div><i class="fas fa-home"></i></div>
 | 
								<div><i class="fas fa-home"></i></div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<span>{{ $ts._visibility.home }}</span>
 | 
									<span>{{ $ts._visibility.home }}</span>
 | 
				
			||||||
				<span>{{ $ts._visibility.homeDescription }}</span>
 | 
									<span>{{ $ts._visibility.homeDescription }}</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</button>
 | 
							</button>
 | 
				
			||||||
		<button key="followers" class="_button" :class="{ active: v == 'followers' }" data-index="3" @click="choose('followers')">
 | 
							<button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
 | 
				
			||||||
			<div><i class="fas fa-unlock"></i></div>
 | 
								<div><i class="fas fa-unlock"></i></div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<span>{{ $ts._visibility.followers }}</span>
 | 
									<span>{{ $ts._visibility.followers }}</span>
 | 
				
			||||||
				<span>{{ $ts._visibility.followersDescription }}</span>
 | 
									<span>{{ $ts._visibility.followersDescription }}</span>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</button>
 | 
							</button>
 | 
				
			||||||
		<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v == 'specified' }" data-index="4" @click="choose('specified')">
 | 
							<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
 | 
				
			||||||
			<div><i class="fas fa-envelope"></i></div>
 | 
								<div><i class="fas fa-envelope"></i></div>
 | 
				
			||||||
			<div>
 | 
								<div>
 | 
				
			||||||
				<span>{{ $ts._visibility.specified }}</span>
 | 
									<span>{{ $ts._visibility.specified }}</span>
 | 
				
			||||||
| 
						 | 
					@ -42,49 +42,40 @@
 | 
				
			||||||
</MkModal>
 | 
					</MkModal>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { nextTick, watch } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import MkModal from '@/components/ui/modal.vue';
 | 
					import MkModal from '@/components/ui/modal.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const modal = $ref<InstanceType<typeof MkModal>>();
 | 
				
			||||||
	components: {
 | 
					
 | 
				
			||||||
		MkModal,
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	},
 | 
						currentVisibility: typeof misskey.noteVisibilities[number];
 | 
				
			||||||
	props: {
 | 
						currentLocalOnly: boolean;
 | 
				
			||||||
		currentVisibility: {
 | 
						src?: HTMLElement;
 | 
				
			||||||
			type: String,
 | 
					}>(), {
 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		currentLocalOnly: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		src: {
 | 
					 | 
				
			||||||
			required: false
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	emits: ['change-visibility', 'change-local-only', 'closed'],
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			v: this.currentVisibility,
 | 
					 | 
				
			||||||
			localOnly: this.currentLocalOnly,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	watch: {
 | 
					 | 
				
			||||||
		localOnly() {
 | 
					 | 
				
			||||||
			this.$emit('change-local-only', this.localOnly);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		choose(visibility) {
 | 
					 | 
				
			||||||
			this.v = visibility;
 | 
					 | 
				
			||||||
			this.$emit('change-visibility', visibility);
 | 
					 | 
				
			||||||
			this.$nextTick(() => {
 | 
					 | 
				
			||||||
				this.$refs.modal.close();
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
 | 
				
			||||||
 | 
						(e: 'changeLocalOnly', v: boolean): void;
 | 
				
			||||||
 | 
						(e: 'closed'): void;
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let v = $ref(props.currentVisibility);
 | 
				
			||||||
 | 
					let localOnly = $ref(props.currentLocalOnly);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch($$(localOnly), () => {
 | 
				
			||||||
 | 
						emit('changeLocalOnly', localOnly);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function choose(visibility: typeof misskey.noteVisibilities[number]): void {
 | 
				
			||||||
 | 
						v = visibility;
 | 
				
			||||||
 | 
						emit('changeVisibility', visibility);
 | 
				
			||||||
 | 
						nextTick(() => {
 | 
				
			||||||
 | 
							modal.close();
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="$emit('closed')">
 | 
					<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
 | 
				
			||||||
	<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }">
 | 
						<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }">
 | 
				
			||||||
		<i v-if="success" class="fas fa-check icon success"></i>
 | 
							<i v-if="success" class="fas fa-check icon success"></i>
 | 
				
			||||||
		<i v-else class="fas fa-spinner fa-pulse icon waiting"></i>
 | 
							<i v-else class="fas fa-spinner fa-pulse icon waiting"></i>
 | 
				
			||||||
| 
						 | 
					@ -8,49 +8,30 @@
 | 
				
			||||||
</MkModal>
 | 
					</MkModal>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { watch, ref } from 'vue';
 | 
				
			||||||
import MkModal from '@/components/ui/modal.vue';
 | 
					import MkModal from '@/components/ui/modal.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const modal = ref<InstanceType<typeof MkModal>>();
 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		MkModal,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	props: {
 | 
					const props = defineProps<{
 | 
				
			||||||
		success: {
 | 
						success: boolean;
 | 
				
			||||||
			type: Boolean,
 | 
						showing: boolean;
 | 
				
			||||||
			required: true,
 | 
						text?: string;
 | 
				
			||||||
		},
 | 
					}>();
 | 
				
			||||||
		showing: {
 | 
					 | 
				
			||||||
			type: Boolean,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		text: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: false,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emits: ['done', 'closed'],
 | 
					const emit = defineEmits<{
 | 
				
			||||||
 | 
						(e: 'done');
 | 
				
			||||||
 | 
						(e: 'closed');
 | 
				
			||||||
 | 
					}>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					function done() {
 | 
				
			||||||
		return {
 | 
						emit('done');
 | 
				
			||||||
		};
 | 
						modal.value.close();
 | 
				
			||||||
	},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	watch: {
 | 
					watch(() => props.showing, () => {
 | 
				
			||||||
		showing() {
 | 
						if (!props.showing) done();
 | 
				
			||||||
			if (!this.showing) this.done();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		done() {
 | 
					 | 
				
			||||||
			this.$emit('done');
 | 
					 | 
				
			||||||
			this.$refs.modal.close();
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ export default {
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted(src, binding, vn) {
 | 
						mounted(src, binding, vn) {
 | 
				
			||||||
		setTimeout(() => {
 | 
							window.setTimeout(() => {
 | 
				
			||||||
			src.style.opacity = '1';
 | 
								src.style.opacity = '1';
 | 
				
			||||||
			src.style.transform = 'none';
 | 
								src.style.transform = 'none';
 | 
				
			||||||
		}, 1);
 | 
							}, 1);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		self.close = () => {
 | 
							self.close = () => {
 | 
				
			||||||
			if (self._close) {
 | 
								if (self._close) {
 | 
				
			||||||
				clearInterval(self.checkTimer);
 | 
									window.clearInterval(self.checkTimer);
 | 
				
			||||||
				self._close();
 | 
									self._close();
 | 
				
			||||||
				self._close = null;
 | 
									self._close = null;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -61,19 +61,19 @@ export default {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		el.addEventListener(start, () => {
 | 
							el.addEventListener(start, () => {
 | 
				
			||||||
			clearTimeout(self.showTimer);
 | 
								window.clearTimeout(self.showTimer);
 | 
				
			||||||
			clearTimeout(self.hideTimer);
 | 
								window.clearTimeout(self.hideTimer);
 | 
				
			||||||
			self.showTimer = setTimeout(self.show, delay);
 | 
								self.showTimer = window.setTimeout(self.show, delay);
 | 
				
			||||||
		}, { passive: true });
 | 
							}, { passive: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		el.addEventListener(end, () => {
 | 
							el.addEventListener(end, () => {
 | 
				
			||||||
			clearTimeout(self.showTimer);
 | 
								window.clearTimeout(self.showTimer);
 | 
				
			||||||
			clearTimeout(self.hideTimer);
 | 
								window.clearTimeout(self.hideTimer);
 | 
				
			||||||
			self.hideTimer = setTimeout(self.close, delay);
 | 
								self.hideTimer = window.setTimeout(self.close, delay);
 | 
				
			||||||
		}, { passive: true });
 | 
							}, { passive: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		el.addEventListener('click', () => {
 | 
							el.addEventListener('click', () => {
 | 
				
			||||||
			clearTimeout(self.showTimer);
 | 
								window.clearTimeout(self.showTimer);
 | 
				
			||||||
			self.close();
 | 
								self.close();
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
| 
						 | 
					@ -85,6 +85,6 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unmounted(el, binding, vn) {
 | 
						unmounted(el, binding, vn) {
 | 
				
			||||||
		const self = el._tooltipDirective_;
 | 
							const self = el._tooltipDirective_;
 | 
				
			||||||
		clearInterval(self.checkTimer);
 | 
							window.clearInterval(self.checkTimer);
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
} as Directive;
 | 
					} as Directive;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,11 +30,11 @@ export class UserPreview {
 | 
				
			||||||
			source: this.el
 | 
								source: this.el
 | 
				
			||||||
		}, {
 | 
							}, {
 | 
				
			||||||
			mouseover: () => {
 | 
								mouseover: () => {
 | 
				
			||||||
				clearTimeout(this.hideTimer);
 | 
									window.clearTimeout(this.hideTimer);
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			mouseleave: () => {
 | 
								mouseleave: () => {
 | 
				
			||||||
				clearTimeout(this.showTimer);
 | 
									window.clearTimeout(this.showTimer);
 | 
				
			||||||
				this.hideTimer = setTimeout(this.close, 500);
 | 
									this.hideTimer = window.setTimeout(this.close, 500);
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}, 'closed');
 | 
							}, 'closed');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,10 +44,10 @@ export class UserPreview {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.checkTimer = setInterval(() => {
 | 
							this.checkTimer = window.setInterval(() => {
 | 
				
			||||||
			if (!document.body.contains(this.el)) {
 | 
								if (!document.body.contains(this.el)) {
 | 
				
			||||||
				clearTimeout(this.showTimer);
 | 
									window.clearTimeout(this.showTimer);
 | 
				
			||||||
				clearTimeout(this.hideTimer);
 | 
									window.clearTimeout(this.hideTimer);
 | 
				
			||||||
				this.close();
 | 
									this.close();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}, 1000);
 | 
							}, 1000);
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ export class UserPreview {
 | 
				
			||||||
	@autobind
 | 
						@autobind
 | 
				
			||||||
	private close() {
 | 
						private close() {
 | 
				
			||||||
		if (this.promise) {
 | 
							if (this.promise) {
 | 
				
			||||||
			clearInterval(this.checkTimer);
 | 
								window.clearInterval(this.checkTimer);
 | 
				
			||||||
			this.promise.cancel();
 | 
								this.promise.cancel();
 | 
				
			||||||
			this.promise = null;
 | 
								this.promise = null;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -64,21 +64,21 @@ export class UserPreview {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@autobind
 | 
						@autobind
 | 
				
			||||||
	private onMouseover() {
 | 
						private onMouseover() {
 | 
				
			||||||
		clearTimeout(this.showTimer);
 | 
							window.clearTimeout(this.showTimer);
 | 
				
			||||||
		clearTimeout(this.hideTimer);
 | 
							window.clearTimeout(this.hideTimer);
 | 
				
			||||||
		this.showTimer = setTimeout(this.show, 500);
 | 
							this.showTimer = window.setTimeout(this.show, 500);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@autobind
 | 
						@autobind
 | 
				
			||||||
	private onMouseleave() {
 | 
						private onMouseleave() {
 | 
				
			||||||
		clearTimeout(this.showTimer);
 | 
							window.clearTimeout(this.showTimer);
 | 
				
			||||||
		clearTimeout(this.hideTimer);
 | 
							window.clearTimeout(this.hideTimer);
 | 
				
			||||||
		this.hideTimer = setTimeout(this.close, 500);
 | 
							this.hideTimer = window.setTimeout(this.close, 500);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@autobind
 | 
						@autobind
 | 
				
			||||||
	private onClick() {
 | 
						private onClick() {
 | 
				
			||||||
		clearTimeout(this.showTimer);
 | 
							window.clearTimeout(this.showTimer);
 | 
				
			||||||
		this.close();
 | 
							this.close();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -94,7 +94,7 @@ export class UserPreview {
 | 
				
			||||||
		this.el.removeEventListener('mouseover', this.onMouseover);
 | 
							this.el.removeEventListener('mouseover', this.onMouseover);
 | 
				
			||||||
		this.el.removeEventListener('mouseleave', this.onMouseleave);
 | 
							this.el.removeEventListener('mouseleave', this.onMouseleave);
 | 
				
			||||||
		this.el.removeEventListener('click', this.onClick);
 | 
							this.el.removeEventListener('click', this.onClick);
 | 
				
			||||||
		clearInterval(this.checkTimer);
 | 
							window.clearInterval(this.checkTimer);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@ export function promiseDialog<T extends Promise<any>>(
 | 
				
			||||||
			onSuccess(res);
 | 
								onSuccess(res);
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			success.value = true;
 | 
								success.value = true;
 | 
				
			||||||
			setTimeout(() => {
 | 
								window.setTimeout(() => {
 | 
				
			||||||
				showing.value = false;
 | 
									showing.value = false;
 | 
				
			||||||
			}, 1000);
 | 
								}, 1000);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -139,7 +139,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
 | 
				
			||||||
	const id = ++popupIdCount;
 | 
						const id = ++popupIdCount;
 | 
				
			||||||
	const dispose = () => {
 | 
						const dispose = () => {
 | 
				
			||||||
		// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
 | 
							// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
 | 
				
			||||||
		setTimeout(() => {
 | 
							window.setTimeout(() => {
 | 
				
			||||||
			popups.value = popups.value.filter(popup => popup.id !== id);
 | 
								popups.value = popups.value.filter(popup => popup.id !== id);
 | 
				
			||||||
		}, 0);
 | 
							}, 0);
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
| 
						 | 
					@ -329,7 +329,7 @@ export function select(props: {
 | 
				
			||||||
export function success() {
 | 
					export function success() {
 | 
				
			||||||
	return new Promise((resolve, reject) => {
 | 
						return new Promise((resolve, reject) => {
 | 
				
			||||||
		const showing = ref(true);
 | 
							const showing = ref(true);
 | 
				
			||||||
		setTimeout(() => {
 | 
							window.setTimeout(() => {
 | 
				
			||||||
			showing.value = false;
 | 
								showing.value = false;
 | 
				
			||||||
		}, 1000);
 | 
							}, 1000);
 | 
				
			||||||
		popup(import('@/components/waiting-dialog.vue'), {
 | 
							popup(import('@/components/waiting-dialog.vue'), {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,68 +1,61 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<MkLoading v-if="!loaded" />
 | 
					<MkLoading v-if="!loaded"/>
 | 
				
			||||||
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
 | 
					<transition :name="$store.state.animation ? 'zoom' : ''" appear>
 | 
				
			||||||
	<div v-show="loaded" class="mjndxjch">
 | 
						<div v-show="loaded" class="mjndxjch">
 | 
				
			||||||
		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
 | 
							<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
 | 
				
			||||||
		<p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p>
 | 
							<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p>
 | 
				
			||||||
		<p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p>
 | 
							<p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p>
 | 
				
			||||||
		<p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p>
 | 
							<p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p>
 | 
				
			||||||
		<template v-else>
 | 
							<template v-else>
 | 
				
			||||||
			<p>{{ $ts.newVersionOfClientAvailable }}</p>
 | 
								<p>{{ i18n.locale.newVersionOfClientAvailable }}</p>
 | 
				
			||||||
			<p>{{ $ts.youShouldUpgradeClient }}</p>
 | 
								<p>{{ i18n.locale.youShouldUpgradeClient }}</p>
 | 
				
			||||||
			<MkButton class="button primary" @click="reload">{{ $ts.reload }}</MkButton>
 | 
								<MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton>
 | 
				
			||||||
		</template>
 | 
							</template>
 | 
				
			||||||
		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p>
 | 
							<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p>
 | 
				
			||||||
		<p v-if="error" class="error">ERROR: {{ error }}</p>
 | 
							<p v-if="error" class="error">ERROR: {{ error }}</p>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</transition>
 | 
					</transition>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
 | 
					import * as misskey from 'misskey-js';
 | 
				
			||||||
import MkButton from '@/components/ui/button.vue';
 | 
					import MkButton from '@/components/ui/button.vue';
 | 
				
			||||||
import * as symbols from '@/symbols';
 | 
					import * as symbols from '@/symbols';
 | 
				
			||||||
import { version } from '@/config';
 | 
					import { version } from '@/config';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import { unisonReload } from '@/scripts/unison-reload';
 | 
					import { unisonReload } from '@/scripts/unison-reload';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = withDefaults(defineProps<{
 | 
				
			||||||
	components: {
 | 
						error?: Error;
 | 
				
			||||||
		MkButton,
 | 
					}>(), {
 | 
				
			||||||
	},
 | 
					});
 | 
				
			||||||
	props: {
 | 
					
 | 
				
			||||||
		error: {
 | 
					let loaded = $ref(false);
 | 
				
			||||||
			required: false,
 | 
					let serverIsDead = $ref(false);
 | 
				
			||||||
		}
 | 
					let meta = $ref<misskey.entities.LiteInstanceMetadata | null>(null);
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
	data() {
 | 
					os.api('meta', {
 | 
				
			||||||
		return {
 | 
						detail: false,
 | 
				
			||||||
			[symbols.PAGE_INFO]: {
 | 
					}).then(res => {
 | 
				
			||||||
				title: this.$ts.error,
 | 
						loaded = true;
 | 
				
			||||||
				icon: 'fas fa-exclamation-triangle'
 | 
						serverIsDead = false;
 | 
				
			||||||
			},
 | 
						meta = res;
 | 
				
			||||||
			loaded: false,
 | 
						localStorage.setItem('v', res.version);
 | 
				
			||||||
			serverIsDead: false,
 | 
					}, () => {
 | 
				
			||||||
			meta: {} as any,
 | 
						loaded = true;
 | 
				
			||||||
			version,
 | 
						serverIsDead = true;
 | 
				
			||||||
		};
 | 
					});
 | 
				
			||||||
	},
 | 
					
 | 
				
			||||||
	created() {
 | 
					function reload() {
 | 
				
			||||||
		os.api('meta', {
 | 
					 | 
				
			||||||
			detail: false
 | 
					 | 
				
			||||||
		}).then(meta => {
 | 
					 | 
				
			||||||
			this.loaded = true;
 | 
					 | 
				
			||||||
			this.serverIsDead = false;
 | 
					 | 
				
			||||||
			this.meta = meta;
 | 
					 | 
				
			||||||
			localStorage.setItem('v', meta.version);
 | 
					 | 
				
			||||||
		}, () => {
 | 
					 | 
				
			||||||
			this.loaded = true;
 | 
					 | 
				
			||||||
			this.serverIsDead = true;
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		reload() {
 | 
					 | 
				
			||||||
	unisonReload();
 | 
						unisonReload();
 | 
				
			||||||
		},
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
						[symbols.PAGE_INFO]: {
 | 
				
			||||||
 | 
							title: i18n.locale.error,
 | 
				
			||||||
 | 
							icon: 'fas fa-exclamation-triangle',
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export default defineComponent({
 | 
				
			||||||
			reporterOrigin: 'combined',
 | 
								reporterOrigin: 'combined',
 | 
				
			||||||
			targetUserOrigin: 'combined',
 | 
								targetUserOrigin: 'combined',
 | 
				
			||||||
			pagination: {
 | 
								pagination: {
 | 
				
			||||||
				endpoint: 'admin/abuse-user-reports',
 | 
									endpoint: 'admin/abuse-user-reports' as const,
 | 
				
			||||||
				limit: 10,
 | 
									limit: 10,
 | 
				
			||||||
				params: computed(() => ({
 | 
									params: computed(() => ({
 | 
				
			||||||
					state: this.state,
 | 
										state: this.state,
 | 
				
			||||||
| 
						 | 
					@ -106,10 +106,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		acct,
 | 
							acct,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -87,10 +87,6 @@ export default defineComponent({
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		add() {
 | 
							add() {
 | 
				
			||||||
			this.ads.unshift({
 | 
								this.ads.unshift({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,10 +61,6 @@ export default defineComponent({
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		add() {
 | 
							add() {
 | 
				
			||||||
			this.announcements.unshift({
 | 
								this.announcements.unshift({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,10 +82,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,10 +37,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		bytes, number,
 | 
							bytes, number,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,10 +93,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export default defineComponent({
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			if (canceled) return;
 | 
								if (canceled) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			os.api('admin/emoji/remove', {
 | 
								os.api('admin/emoji/delete', {
 | 
				
			||||||
				id: this.emoji.id
 | 
									id: this.emoji.id
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				this.$emit('done', {
 | 
									this.$emit('done', {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,11 +6,22 @@
 | 
				
			||||||
				<template #prefix><i class="fas fa-search"></i></template>
 | 
									<template #prefix><i class="fas fa-search"></i></template>
 | 
				
			||||||
				<template #label>{{ $ts.search }}</template>
 | 
									<template #label>{{ $ts.search }}</template>
 | 
				
			||||||
			</MkInput>
 | 
								</MkInput>
 | 
				
			||||||
			<MkPagination ref="emojis" :pagination="pagination">
 | 
								<MkSwitch v-model="selectMode" style="margin: 8px 0;">
 | 
				
			||||||
 | 
									<template #label>Select mode</template>
 | 
				
			||||||
 | 
								</MkSwitch>
 | 
				
			||||||
 | 
								<div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
 | 
				
			||||||
 | 
									<MkButton inline @click="selectAll">Select all</MkButton>
 | 
				
			||||||
 | 
									<MkButton inline @click="setCategoryBulk">Set category</MkButton>
 | 
				
			||||||
 | 
									<MkButton inline @click="addTagBulk">Add tag</MkButton>
 | 
				
			||||||
 | 
									<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
 | 
				
			||||||
 | 
									<MkButton inline @click="setTagBulk">Set tag</MkButton>
 | 
				
			||||||
 | 
									<MkButton inline danger @click="delBulk">Delete</MkButton>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
 | 
				
			||||||
				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
 | 
									<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
 | 
				
			||||||
				<template v-slot="{items}">
 | 
									<template v-slot="{items}">
 | 
				
			||||||
					<div class="ldhfsamy">
 | 
										<div class="ldhfsamy">
 | 
				
			||||||
						<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
 | 
											<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
 | 
				
			||||||
							<img :src="emoji.url" class="img" :alt="emoji.name"/>
 | 
												<img :src="emoji.url" class="img" :alt="emoji.name"/>
 | 
				
			||||||
							<div class="body">
 | 
												<div class="body">
 | 
				
			||||||
								<div class="name _monospace">{{ emoji.name }}</div>
 | 
													<div class="name _monospace">{{ emoji.name }}</div>
 | 
				
			||||||
| 
						 | 
					@ -32,7 +43,7 @@
 | 
				
			||||||
					<template #label>{{ $ts.host }}</template>
 | 
										<template #label>{{ $ts.host }}</template>
 | 
				
			||||||
				</MkInput>
 | 
									</MkInput>
 | 
				
			||||||
			</FormSplit>
 | 
								</FormSplit>
 | 
				
			||||||
			<MkPagination ref="remoteEmojis" :pagination="remotePagination">
 | 
								<MkPagination :pagination="remotePagination">
 | 
				
			||||||
				<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
 | 
									<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
 | 
				
			||||||
				<template v-slot="{items}">
 | 
									<template v-slot="{items}">
 | 
				
			||||||
					<div class="ldhfsamy">
 | 
										<div class="ldhfsamy">
 | 
				
			||||||
| 
						 | 
					@ -51,137 +62,138 @@
 | 
				
			||||||
</MkSpacer>
 | 
					</MkSpacer>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, defineComponent, toRef } from 'vue';
 | 
					import { computed, defineComponent, ref, toRef } from 'vue';
 | 
				
			||||||
import MkButton from '@/components/ui/button.vue';
 | 
					import MkButton from '@/components/ui/button.vue';
 | 
				
			||||||
import MkInput from '@/components/form/input.vue';
 | 
					import MkInput from '@/components/form/input.vue';
 | 
				
			||||||
import MkPagination from '@/components/ui/pagination.vue';
 | 
					import MkPagination from '@/components/ui/pagination.vue';
 | 
				
			||||||
import MkTab from '@/components/tab.vue';
 | 
					import MkTab from '@/components/tab.vue';
 | 
				
			||||||
 | 
					import MkSwitch from '@/components/form/switch.vue';
 | 
				
			||||||
import FormSplit from '@/components/form/split.vue';
 | 
					import FormSplit from '@/components/form/split.vue';
 | 
				
			||||||
import { selectFiles } from '@/scripts/select-file';
 | 
					import { selectFile, selectFiles } from '@/scripts/select-file';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import * as symbols from '@/symbols';
 | 
					import * as symbols from '@/symbols';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		MkTab,
 | 
					 | 
				
			||||||
		MkButton,
 | 
					 | 
				
			||||||
		MkInput,
 | 
					 | 
				
			||||||
		MkPagination,
 | 
					 | 
				
			||||||
		FormSplit,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	emits: ['info'],
 | 
					const tab = ref('local');
 | 
				
			||||||
 | 
					const query = ref(null);
 | 
				
			||||||
 | 
					const queryRemote = ref(null);
 | 
				
			||||||
 | 
					const host = ref(null);
 | 
				
			||||||
 | 
					const selectMode = ref(false);
 | 
				
			||||||
 | 
					const selectedEmojis = ref<string[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					const pagination = {
 | 
				
			||||||
		return {
 | 
						endpoint: 'admin/emoji/list' as const,
 | 
				
			||||||
			[symbols.PAGE_INFO]: computed(() => ({
 | 
						limit: 30,
 | 
				
			||||||
				title: this.$ts.customEmojis,
 | 
						params: computed(() => ({
 | 
				
			||||||
				icon: 'fas fa-laugh',
 | 
							query: (query.value && query.value !== '') ? query.value : null,
 | 
				
			||||||
				bg: 'var(--bg)',
 | 
					 | 
				
			||||||
				actions: [{
 | 
					 | 
				
			||||||
					asFullButton: true,
 | 
					 | 
				
			||||||
					icon: 'fas fa-plus',
 | 
					 | 
				
			||||||
					text: this.$ts.addEmoji,
 | 
					 | 
				
			||||||
					handler: this.add,
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					icon: 'fas fa-ellipsis-h',
 | 
					 | 
				
			||||||
					handler: this.menu,
 | 
					 | 
				
			||||||
				}],
 | 
					 | 
				
			||||||
				tabs: [{
 | 
					 | 
				
			||||||
					active: this.tab === 'local',
 | 
					 | 
				
			||||||
					title: this.$ts.local,
 | 
					 | 
				
			||||||
					onClick: () => { this.tab = 'local'; },
 | 
					 | 
				
			||||||
				}, {
 | 
					 | 
				
			||||||
					active: this.tab === 'remote',
 | 
					 | 
				
			||||||
					title: this.$ts.remote,
 | 
					 | 
				
			||||||
					onClick: () => { this.tab = 'remote'; },
 | 
					 | 
				
			||||||
				},]
 | 
					 | 
				
			||||||
	})),
 | 
						})),
 | 
				
			||||||
			tab: 'local',
 | 
					};
 | 
				
			||||||
			query: null,
 | 
					
 | 
				
			||||||
			queryRemote: null,
 | 
					const remotePagination = {
 | 
				
			||||||
			host: '',
 | 
						endpoint: 'admin/emoji/list-remote' as const,
 | 
				
			||||||
			pagination: {
 | 
					 | 
				
			||||||
				endpoint: 'admin/emoji/list',
 | 
					 | 
				
			||||||
	limit: 30,
 | 
						limit: 30,
 | 
				
			||||||
	params: computed(() => ({
 | 
						params: computed(() => ({
 | 
				
			||||||
					query: (this.query && this.query !== '') ? this.query : null
 | 
							query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
 | 
				
			||||||
				}))
 | 
							host: (host.value && host.value !== '') ? host.value : null,
 | 
				
			||||||
			},
 | 
						})),
 | 
				
			||||||
			remotePagination: {
 | 
					};
 | 
				
			||||||
				endpoint: 'admin/emoji/list-remote',
 | 
					
 | 
				
			||||||
				limit: 30,
 | 
					const selectAll = () => {
 | 
				
			||||||
				params: computed(() => ({
 | 
						if (selectedEmojis.value.length > 0) {
 | 
				
			||||||
					query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
 | 
							selectedEmojis.value = [];
 | 
				
			||||||
					host: (this.host && this.host !== '') ? this.host : null
 | 
						} else {
 | 
				
			||||||
				}))
 | 
							selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	},
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					const toggleSelect = (emoji) => {
 | 
				
			||||||
		this.$emit('info', toRef(this, symbols.PAGE_INFO));
 | 
						if (selectedEmojis.value.includes(emoji.id)) {
 | 
				
			||||||
	},
 | 
							selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							selectedEmojis.value.push(emoji.id);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	methods: {
 | 
					const add = async (ev: MouseEvent) => {
 | 
				
			||||||
		async add(e) {
 | 
						const files = await selectFiles(ev.currentTarget || ev.target, null);
 | 
				
			||||||
			const files = await selectFiles(e.currentTarget || e.target, null);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
 | 
						const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
 | 
				
			||||||
		fileId: file.id,
 | 
							fileId: file.id,
 | 
				
			||||||
	})));
 | 
						})));
 | 
				
			||||||
	promise.then(() => {
 | 
						promise.then(() => {
 | 
				
			||||||
				this.$refs.emojis.reload();
 | 
							emojisPaginationComponent.value.reload();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	os.promiseDialog(promise);
 | 
						os.promiseDialog(promise);
 | 
				
			||||||
		},
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		edit(emoji) {
 | 
					const edit = (emoji) => {
 | 
				
			||||||
	os.popup(import('./emoji-edit-dialog.vue'), {
 | 
						os.popup(import('./emoji-edit-dialog.vue'), {
 | 
				
			||||||
		emoji: emoji
 | 
							emoji: emoji
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
		done: result => {
 | 
							done: result => {
 | 
				
			||||||
			if (result.updated) {
 | 
								if (result.updated) {
 | 
				
			||||||
						this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
 | 
									emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
 | 
				
			||||||
					...emoji,
 | 
										...emoji,
 | 
				
			||||||
					...result.updated
 | 
										...result.updated
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			} else if (result.deleted) {
 | 
								} else if (result.deleted) {
 | 
				
			||||||
						this.$refs.emojis.removeItem(item => item.id === emoji.id);
 | 
									emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}, 'closed');
 | 
						}, 'closed');
 | 
				
			||||||
		},
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		im(emoji) {
 | 
					const im = (emoji) => {
 | 
				
			||||||
	os.apiWithDialog('admin/emoji/copy', {
 | 
						os.apiWithDialog('admin/emoji/copy', {
 | 
				
			||||||
		emojiId: emoji.id,
 | 
							emojiId: emoji.id,
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
		},
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		remoteMenu(emoji, ev) {
 | 
					const remoteMenu = (emoji, ev: MouseEvent) => {
 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
		type: 'label',
 | 
							type: 'label',
 | 
				
			||||||
		text: ':' + emoji.name + ':',
 | 
							text: ':' + emoji.name + ':',
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
				text: this.$ts.import,
 | 
							text: i18n.locale.import,
 | 
				
			||||||
		icon: 'fas fa-plus',
 | 
							icon: 'fas fa-plus',
 | 
				
			||||||
				action: () => { this.im(emoji) }
 | 
							action: () => { im(emoji) }
 | 
				
			||||||
	}], ev.currentTarget || ev.target);
 | 
						}], ev.currentTarget || ev.target);
 | 
				
			||||||
		},
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		menu(ev) {
 | 
					const menu = (ev: MouseEvent) => {
 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
		icon: 'fas fa-download',
 | 
							icon: 'fas fa-download',
 | 
				
			||||||
				text: this.$ts.export,
 | 
							text: i18n.locale.export,
 | 
				
			||||||
		action: async () => {
 | 
							action: async () => {
 | 
				
			||||||
			os.api('export-custom-emojis', {
 | 
								os.api('export-custom-emojis', {
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			.then(() => {
 | 
								.then(() => {
 | 
				
			||||||
				os.alert({
 | 
									os.alert({
 | 
				
			||||||
					type: 'info',
 | 
										type: 'info',
 | 
				
			||||||
							text: this.$ts.exportRequested,
 | 
										text: i18n.locale.exportRequested,
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}).catch((e) => {
 | 
				
			||||||
 | 
									os.alert({
 | 
				
			||||||
 | 
										type: 'error',
 | 
				
			||||||
 | 
										text: e.message,
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							icon: 'fas fa-upload',
 | 
				
			||||||
 | 
							text: i18n.locale.import,
 | 
				
			||||||
 | 
							action: async () => {
 | 
				
			||||||
 | 
								const file = await selectFile(ev.currentTarget || ev.target);
 | 
				
			||||||
 | 
								os.api('admin/emoji/import-zip', {
 | 
				
			||||||
 | 
									fileId: file.id,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.then(() => {
 | 
				
			||||||
 | 
									os.alert({
 | 
				
			||||||
 | 
										type: 'info',
 | 
				
			||||||
 | 
										text: i18n.locale.importRequested,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}).catch((e) => {
 | 
								}).catch((e) => {
 | 
				
			||||||
				os.alert({
 | 
									os.alert({
 | 
				
			||||||
| 
						 | 
					@ -191,8 +203,92 @@ export default defineComponent({
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}], ev.currentTarget || ev.target);
 | 
						}], ev.currentTarget || ev.target);
 | 
				
			||||||
		}
 | 
					};
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
 | 
					const setCategoryBulk = async () => {
 | 
				
			||||||
 | 
						const { canceled, result } = await os.inputText({
 | 
				
			||||||
 | 
							title: 'Category',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/emoji/set-category-bulk', {
 | 
				
			||||||
 | 
							ids: selectedEmojis.value,
 | 
				
			||||||
 | 
							category: result,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						emojisPaginationComponent.value.reload();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const addTagBulk = async () => {
 | 
				
			||||||
 | 
						const { canceled, result } = await os.inputText({
 | 
				
			||||||
 | 
							title: 'Tag',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
 | 
				
			||||||
 | 
							ids: selectedEmojis.value,
 | 
				
			||||||
 | 
							aliases: result.split(' '),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						emojisPaginationComponent.value.reload();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const removeTagBulk = async () => {
 | 
				
			||||||
 | 
						const { canceled, result } = await os.inputText({
 | 
				
			||||||
 | 
							title: 'Tag',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
 | 
				
			||||||
 | 
							ids: selectedEmojis.value,
 | 
				
			||||||
 | 
							aliases: result.split(' '),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						emojisPaginationComponent.value.reload();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const setTagBulk = async () => {
 | 
				
			||||||
 | 
						const { canceled, result } = await os.inputText({
 | 
				
			||||||
 | 
							title: 'Tag',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
 | 
				
			||||||
 | 
							ids: selectedEmojis.value,
 | 
				
			||||||
 | 
							aliases: result.split(' '),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						emojisPaginationComponent.value.reload();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const delBulk = async () => {
 | 
				
			||||||
 | 
						const { canceled } = await os.confirm({
 | 
				
			||||||
 | 
							type: 'warning',
 | 
				
			||||||
 | 
							text: i18n.locale.deleteConfirm,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/emoji/delete-bulk', {
 | 
				
			||||||
 | 
							ids: selectedEmojis.value,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						emojisPaginationComponent.value.reload();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
						[symbols.PAGE_INFO]: computed(() => ({
 | 
				
			||||||
 | 
							title: i18n.locale.customEmojis,
 | 
				
			||||||
 | 
							icon: 'fas fa-laugh',
 | 
				
			||||||
 | 
							bg: 'var(--bg)',
 | 
				
			||||||
 | 
							actions: [{
 | 
				
			||||||
 | 
								asFullButton: true,
 | 
				
			||||||
 | 
								icon: 'fas fa-plus',
 | 
				
			||||||
 | 
								text: i18n.locale.addEmoji,
 | 
				
			||||||
 | 
								handler: add,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								icon: 'fas fa-ellipsis-h',
 | 
				
			||||||
 | 
								handler: menu,
 | 
				
			||||||
 | 
							}],
 | 
				
			||||||
 | 
							tabs: [{
 | 
				
			||||||
 | 
								active: tab.value === 'local',
 | 
				
			||||||
 | 
								title: i18n.locale.local,
 | 
				
			||||||
 | 
								onClick: () => { tab.value = 'local'; },
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								active: tab.value === 'remote',
 | 
				
			||||||
 | 
								title: i18n.locale.remote,
 | 
				
			||||||
 | 
								onClick: () => { tab.value = 'remote'; },
 | 
				
			||||||
 | 
							},]
 | 
				
			||||||
 | 
						})),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -212,11 +308,16 @@ export default defineComponent({
 | 
				
			||||||
			> .emoji {
 | 
								> .emoji {
 | 
				
			||||||
				display: flex;
 | 
									display: flex;
 | 
				
			||||||
				align-items: center;
 | 
									align-items: center;
 | 
				
			||||||
				padding: 12px;
 | 
									padding: 11px;
 | 
				
			||||||
				text-align: left;
 | 
									text-align: left;
 | 
				
			||||||
 | 
									border: solid 1px var(--panel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&:hover {
 | 
									&:hover {
 | 
				
			||||||
					color: var(--accent);
 | 
										border-color: var(--inputBorderHover);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&.selected {
 | 
				
			||||||
 | 
										border-color: var(--accent);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				> .img {
 | 
									> .img {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export default defineComponent({
 | 
				
			||||||
			type: null,
 | 
								type: null,
 | 
				
			||||||
			searchHost: '',
 | 
								searchHost: '',
 | 
				
			||||||
			pagination: {
 | 
								pagination: {
 | 
				
			||||||
				endpoint: 'admin/drive/files',
 | 
									endpoint: 'admin/drive/files' as const,
 | 
				
			||||||
				limit: 10,
 | 
									limit: 10,
 | 
				
			||||||
				params: computed(() => ({
 | 
									params: computed(() => ({
 | 
				
			||||||
					type: (this.type && this.type !== '') ? this.type : null,
 | 
										type: (this.type && this.type !== '') ? this.type : null,
 | 
				
			||||||
| 
						 | 
					@ -106,10 +106,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		clear() {
 | 
							clear() {
 | 
				
			||||||
			os.confirm({
 | 
								os.confirm({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@
 | 
				
			||||||
	<div class="main">
 | 
						<div class="main">
 | 
				
			||||||
		<MkStickyContainer>
 | 
							<MkStickyContainer>
 | 
				
			||||||
			<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
 | 
								<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
 | 
				
			||||||
			<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
 | 
								<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
 | 
				
			||||||
		</MkStickyContainer>
 | 
							</MkStickyContainer>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -66,7 +66,9 @@ export default defineComponent({
 | 
				
			||||||
		const narrow = ref(false);
 | 
							const narrow = ref(false);
 | 
				
			||||||
		const view = ref(null);
 | 
							const view = ref(null);
 | 
				
			||||||
		const el = ref(null);
 | 
							const el = ref(null);
 | 
				
			||||||
		const onInfo = (viewInfo) => {
 | 
							const pageChanged = (page) => {
 | 
				
			||||||
 | 
								if (page == null) return;
 | 
				
			||||||
 | 
								const viewInfo = page[symbols.PAGE_INFO];
 | 
				
			||||||
			if (isRef(viewInfo)) {
 | 
								if (isRef(viewInfo)) {
 | 
				
			||||||
				watch(viewInfo, () => {
 | 
									watch(viewInfo, () => {
 | 
				
			||||||
					childInfo.value = viewInfo.value;
 | 
										childInfo.value = viewInfo.value;
 | 
				
			||||||
| 
						 | 
					@ -311,7 +313,7 @@ export default defineComponent({
 | 
				
			||||||
			narrow,
 | 
								narrow,
 | 
				
			||||||
			view,
 | 
								view,
 | 
				
			||||||
			el,
 | 
								el,
 | 
				
			||||||
			onInfo,
 | 
								pageChanged,
 | 
				
			||||||
			childInfo,
 | 
								childInfo,
 | 
				
			||||||
			pageProps,
 | 
								pageProps,
 | 
				
			||||||
			component,
 | 
								component,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,10 +40,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,10 +58,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,10 +58,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,10 +58,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,10 +60,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -118,10 +118,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,10 +42,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -116,8 +116,6 @@ export default defineComponent({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
						async mounted() {
 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		os.api('meta', { detail: true }).then(meta => {
 | 
							os.api('meta', { detail: true }).then(meta => {
 | 
				
			||||||
			this.meta = meta;
 | 
								this.meta = meta;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,10 +44,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,8 +38,6 @@ export default defineComponent({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		this.$nextTick(() => {
 | 
							this.$nextTick(() => {
 | 
				
			||||||
			this.connection.send('requestLog', {
 | 
								this.connection.send('requestLog', {
 | 
				
			||||||
				id: Math.random().toString().substr(2, 8),
 | 
									id: Math.random().toString().substr(2, 8),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,10 +48,6 @@ export default defineComponent({
 | 
				
			||||||
		this.refresh();
 | 
							this.refresh();
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async addRelay() {
 | 
							async addRelay() {
 | 
				
			||||||
			const { canceled, result: inbox } = await os.inputText({
 | 
								const { canceled, result: inbox } = await os.inputText({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -70,10 +70,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -197,10 +197,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		async init() {
 | 
							async init() {
 | 
				
			||||||
			const meta = await os.api('meta', { detail: true });
 | 
								const meta = await os.api('meta', { detail: true });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -110,7 +110,7 @@ export default defineComponent({
 | 
				
			||||||
			searchUsername: '',
 | 
								searchUsername: '',
 | 
				
			||||||
			searchHost: '',
 | 
								searchHost: '',
 | 
				
			||||||
			pagination: {
 | 
								pagination: {
 | 
				
			||||||
				endpoint: 'admin/show-users',
 | 
									endpoint: 'admin/show-users' as const,
 | 
				
			||||||
				limit: 10,
 | 
									limit: 10,
 | 
				
			||||||
				params: computed(() => ({
 | 
									params: computed(() => ({
 | 
				
			||||||
					sort: this.sort,
 | 
										sort: this.sort,
 | 
				
			||||||
| 
						 | 
					@ -124,10 +124,6 @@ export default defineComponent({
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async mounted() {
 | 
					 | 
				
			||||||
		this.$emit('info', this[symbols.PAGE_INFO]);
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		lookupUser,
 | 
							lookupUser,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ export default defineComponent({
 | 
				
			||||||
				bg: 'var(--bg)',
 | 
									bg: 'var(--bg)',
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			pagination: {
 | 
								pagination: {
 | 
				
			||||||
				endpoint: 'announcements',
 | 
									endpoint: 'announcements' as const,
 | 
				
			||||||
				limit: 10,
 | 
									limit: 10,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@ export default defineComponent({
 | 
				
			||||||
			channel: null,
 | 
								channel: null,
 | 
				
			||||||
			showBanner: true,
 | 
								showBanner: true,
 | 
				
			||||||
			pagination: {
 | 
								pagination: {
 | 
				
			||||||
				endpoint: 'channels/timeline',
 | 
									endpoint: 'channels/timeline' as const,
 | 
				
			||||||
				limit: 10,
 | 
									limit: 10,
 | 
				
			||||||
				params: computed(() => ({
 | 
									params: computed(() => ({
 | 
				
			||||||
					channelId: this.channelId,
 | 
										channelId: this.channelId,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,15 +60,15 @@ export default defineComponent({
 | 
				
			||||||
			})),
 | 
								})),
 | 
				
			||||||
			tab: 'featured',
 | 
								tab: 'featured',
 | 
				
			||||||
			featuredPagination: {
 | 
								featuredPagination: {
 | 
				
			||||||
				endpoint: 'channels/featured',
 | 
									endpoint: 'channels/featured' as const,
 | 
				
			||||||
				noPaging: true,
 | 
									noPaging: true,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			followingPagination: {
 | 
								followingPagination: {
 | 
				
			||||||
				endpoint: 'channels/followed',
 | 
									endpoint: 'channels/followed' as const,
 | 
				
			||||||
				limit: 5,
 | 
									limit: 5,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			ownedPagination: {
 | 
								ownedPagination: {
 | 
				
			||||||
				endpoint: 'channels/owned',
 | 
									endpoint: 'channels/owned' as const,
 | 
				
			||||||
				limit: 5,
 | 
									limit: 5,
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ export default defineComponent({
 | 
				
			||||||
			} : null),
 | 
								} : null),
 | 
				
			||||||
			clip: null,
 | 
								clip: null,
 | 
				
			||||||
			pagination: {
 | 
								pagination: {
 | 
				
			||||||
				endpoint: 'clips/notes',
 | 
									endpoint: 'clips/notes' as const,
 | 
				
			||||||
				limit: 10,
 | 
									limit: 10,
 | 
				
			||||||
				params: computed(() => ({
 | 
									params: computed(() => ({
 | 
				
			||||||
					clipId: this.clipId,
 | 
										clipId: this.clipId,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,27 +4,21 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, defineComponent } from 'vue';
 | 
					import { computed } from 'vue';
 | 
				
			||||||
import XDrive from '@/components/drive.vue';
 | 
					import XDrive from '@/components/drive.vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import * as symbols from '@/symbols';
 | 
					import * as symbols from '@/symbols';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					let folder = $ref(null);
 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		XDrive
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					defineExpose({
 | 
				
			||||||
		return {
 | 
						[symbols.PAGE_INFO]: computed(() => ({
 | 
				
			||||||
			[symbols.PAGE_INFO]: {
 | 
							title: folder ? folder.name : i18n.locale.drive,
 | 
				
			||||||
				title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
 | 
					 | 
				
			||||||
		icon: 'fas fa-cloud',
 | 
							icon: 'fas fa-cloud',
 | 
				
			||||||
		bg: 'var(--bg)',
 | 
							bg: 'var(--bg)',
 | 
				
			||||||
		hideHeader: true,
 | 
							hideHeader: true,
 | 
				
			||||||
			},
 | 
						})),
 | 
				
			||||||
			folder: null,
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,35 +8,29 @@
 | 
				
			||||||
</button>
 | 
					</button>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent } from 'vue';
 | 
					import { } from 'vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
					import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const props = defineProps<{
 | 
				
			||||||
	props: {
 | 
						emoji: Record<string, unknown>; // TODO
 | 
				
			||||||
		emoji: {
 | 
					}>();
 | 
				
			||||||
			type: Object,
 | 
					 | 
				
			||||||
			required: true,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	methods: {
 | 
					function menu(ev) {
 | 
				
			||||||
		menu(ev) {
 | 
					 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
		type: 'label',
 | 
							type: 'label',
 | 
				
			||||||
				text: ':' + this.emoji.name + ':',
 | 
							text: ':' + props.emoji.name + ':',
 | 
				
			||||||
	}, {
 | 
						}, {
 | 
				
			||||||
				text: this.$ts.copy,
 | 
							text: i18n.locale.copy,
 | 
				
			||||||
		icon: 'fas fa-copy',
 | 
							icon: 'fas fa-copy',
 | 
				
			||||||
		action: () => {
 | 
							action: () => {
 | 
				
			||||||
					copyToClipboard(`:${this.emoji.name}:`);
 | 
								copyToClipboard(`:${props.emoji.name}:`);
 | 
				
			||||||
			os.success();
 | 
								os.success();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}], ev.currentTarget || ev.target);
 | 
						}], ev.currentTarget || ev.target);
 | 
				
			||||||
		}
 | 
					}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,44 +4,26 @@
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { defineComponent, computed } from 'vue';
 | 
					import { ref, computed } from 'vue';
 | 
				
			||||||
import * as os from '@/os';
 | 
					import * as os from '@/os';
 | 
				
			||||||
import * as symbols from '@/symbols';
 | 
					import * as symbols from '@/symbols';
 | 
				
			||||||
import XCategory from './emojis.category.vue';
 | 
					import XCategory from './emojis.category.vue';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineComponent({
 | 
					const tab = ref('category');
 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		XCategory,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
					function menu(ev) {
 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			[symbols.PAGE_INFO]: computed(() => ({
 | 
					 | 
				
			||||||
				title: this.$ts.customEmojis,
 | 
					 | 
				
			||||||
				icon: 'fas fa-laugh',
 | 
					 | 
				
			||||||
				bg: 'var(--bg)',
 | 
					 | 
				
			||||||
				actions: [{
 | 
					 | 
				
			||||||
					icon: 'fas fa-ellipsis-h',
 | 
					 | 
				
			||||||
					handler: this.menu
 | 
					 | 
				
			||||||
				}],
 | 
					 | 
				
			||||||
			})),
 | 
					 | 
				
			||||||
			tab: 'category',
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	methods: {
 | 
					 | 
				
			||||||
		menu(ev) {
 | 
					 | 
				
			||||||
	os.popupMenu([{
 | 
						os.popupMenu([{
 | 
				
			||||||
		icon: 'fas fa-download',
 | 
							icon: 'fas fa-download',
 | 
				
			||||||
				text: this.$ts.export,
 | 
							text: i18n.locale.export,
 | 
				
			||||||
		action: async () => {
 | 
							action: async () => {
 | 
				
			||||||
			os.api('export-custom-emojis', {
 | 
								os.api('export-custom-emojis', {
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			.then(() => {
 | 
								.then(() => {
 | 
				
			||||||
				os.alert({
 | 
									os.alert({
 | 
				
			||||||
					type: 'info',
 | 
										type: 'info',
 | 
				
			||||||
							text: this.$ts.exportRequested,
 | 
										text: i18n.locale.exportRequested,
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}).catch((e) => {
 | 
								}).catch((e) => {
 | 
				
			||||||
				os.alert({
 | 
									os.alert({
 | 
				
			||||||
| 
						 | 
					@ -51,8 +33,18 @@ export default defineComponent({
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}], ev.currentTarget || ev.target);
 | 
						}], ev.currentTarget || ev.target);
 | 
				
			||||||
		}
 | 
					}
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
						[symbols.PAGE_INFO]: {
 | 
				
			||||||
 | 
							title: i18n.locale.customEmojis,
 | 
				
			||||||
 | 
							icon: 'fas fa-laugh',
 | 
				
			||||||
 | 
							bg: 'var(--bg)',
 | 
				
			||||||
 | 
							actions: [{
 | 
				
			||||||
 | 
								icon: 'fas fa-ellipsis-h',
 | 
				
			||||||
 | 
								handler: menu,
 | 
				
			||||||
 | 
							}],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue