Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
		
						commit
						3db78e367b
					
				
					 172 changed files with 1968 additions and 3300 deletions
				
			
		| 
						 | 
				
			
			@ -86,7 +86,7 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
 | 
			
		|||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -111,7 +111,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 | 
			
		|||
		logger.succ(`Exported to: ${archivePath}`);
 | 
			
		||||
 | 
			
		||||
		const fileName = 'custom-emojis-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.zip';
 | 
			
		||||
		const driveFile = await addFile(user, archivePath, fileName, null, null, true);
 | 
			
		||||
		const driveFile = await addFile({ user, path: archivePath, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
		cleanup();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,7 +87,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
 | 
			
		|||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
 | 
			
		|||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,7 +95,7 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
 | 
			
		|||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,7 +63,7 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
 | 
			
		|||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile(user, path, fileName, null, null, true);
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
 | 
			
		|||
			await Emojis.delete({
 | 
			
		||||
				name: emojiInfo.name,
 | 
			
		||||
			});
 | 
			
		||||
			const driveFile = await addFile(null, emojiPath, record.fileName, null, null, true);
 | 
			
		||||
			const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
 | 
			
		||||
			const emoji = await Emojis.insert({
 | 
			
		||||
				id: genId(),
 | 
			
		||||
				updatedAt: new Date(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import uploadFromUrl from '@/services/drive/upload-from-url';
 | 
			
		||||
import { uploadFromUrl } from '@/services/drive/upload-from-url';
 | 
			
		||||
import { IRemoteUser } from '@/models/entities/user';
 | 
			
		||||
import Resolver from '../resolver';
 | 
			
		||||
import { fetchMeta } from '@/misc/fetch-meta';
 | 
			
		||||
| 
						 | 
				
			
			@ -28,9 +28,15 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
 | 
			
		|||
	logger.info(`Creating the Image: ${image.url}`);
 | 
			
		||||
 | 
			
		||||
	const instance = await fetchMeta();
 | 
			
		||||
	const cache = instance.cacheRemoteFiles;
 | 
			
		||||
 | 
			
		||||
	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH));
 | 
			
		||||
	let file = await uploadFromUrl({
 | 
			
		||||
		url: image.url,
 | 
			
		||||
		user: actor,
 | 
			
		||||
		uri: image.url,
 | 
			
		||||
		sensitive: image.sensitive,
 | 
			
		||||
		isLink: !instance.cacheRemoteFiles,
 | 
			
		||||
		comment: truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH)
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (file.isLink) {
 | 
			
		||||
		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ export const renderActivity = (x: any): IActivity | null => {
 | 
			
		|||
				PropertyValue: 'schema:PropertyValue',
 | 
			
		||||
				value: 'schema:value',
 | 
			
		||||
				// Misskey
 | 
			
		||||
				misskey: `${config.url}/ns#`,
 | 
			
		||||
				misskey: 'https://misskey-hub.net/ns#',
 | 
			
		||||
				'_misskey_content': 'misskey:_misskey_content',
 | 
			
		||||
				'_misskey_quote': 'misskey:_misskey_quote',
 | 
			
		||||
				'_misskey_reaction': 'misskey:_misskey_reaction',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ export async function resolveUser(username: string, host: string | null, option?
 | 
			
		|||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser;
 | 
			
		||||
	const user = await Users.findOne({ usernameLower, host }, option) as IRemoteUser | null;
 | 
			
		||||
 | 
			
		||||
	const acctLower = `${usernameLower}@${host}`;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,7 +67,7 @@ router.get('/notes/:note', async (ctx, next) => {
 | 
			
		|||
 | 
			
		||||
	const note = await Notes.findOne({
 | 
			
		||||
		id: ctx.params.note,
 | 
			
		||||
		visibility: In(['public', 'home']),
 | 
			
		||||
		visibility: In(['public' as const, 'home' as const]),
 | 
			
		||||
		localOnly: false,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -96,7 +96,7 @@ router.get('/notes/:note/activity', async ctx => {
 | 
			
		|||
	const note = await Notes.findOne({
 | 
			
		||||
		id: ctx.params.note,
 | 
			
		||||
		userHost: null,
 | 
			
		||||
		visibility: In(['public', 'home']),
 | 
			
		||||
		visibility: In(['public' as const, 'home' as const]),
 | 
			
		||||
		localOnly: false,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ export class AuthenticationError extends Error {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => {
 | 
			
		||||
export default async (token: string | null): Promise<[User | null | undefined, AccessToken | null | undefined]> => {
 | 
			
		||||
	if (token == null) {
 | 
			
		||||
		return [null, null];
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import { getConnection } from 'typeorm';
 | 
			
		|||
import { ApiError } from '../../../error';
 | 
			
		||||
import { DriveFile } from '@/models/entities/drive-file';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import uploadFromUrl from '@/services/drive/upload-from-url';
 | 
			
		||||
import { uploadFromUrl } from '@/services/drive/upload-from-url';
 | 
			
		||||
import { publishBroadcastStream } from '@/services/stream';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ export default define(meta, async (ps, me) => {
 | 
			
		|||
 | 
			
		||||
	try {
 | 
			
		||||
		// Create file
 | 
			
		||||
		driveFile = await uploadFromUrl(emoji.originalUrl, null, null, null, false, true);
 | 
			
		||||
		driveFile = await uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		throw new ApiError();
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
	const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
 | 
			
		||||
 | 
			
		||||
	// Create account
 | 
			
		||||
	const app = await Apps.save({
 | 
			
		||||
	const app = await Apps.insert({
 | 
			
		||||
		id: genId(),
 | 
			
		||||
		createdAt: new Date(),
 | 
			
		||||
		userId: user ? user.id : null,
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +55,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
		permission,
 | 
			
		||||
		callbackUrl: ps.callbackUrl,
 | 
			
		||||
		secret: secret,
 | 
			
		||||
	});
 | 
			
		||||
	}).then(x => Apps.findOneOrFail(x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
	return await Apps.pack(app, null, {
 | 
			
		||||
		detail: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,14 +56,14 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const channel = await Channels.save({
 | 
			
		||||
	const channel = await Channels.insert({
 | 
			
		||||
		id: genId(),
 | 
			
		||||
		createdAt: new Date(),
 | 
			
		||||
		userId: user.id,
 | 
			
		||||
		name: ps.name,
 | 
			
		||||
		description: ps.description || null,
 | 
			
		||||
		bannerId: banner ? banner.id : null,
 | 
			
		||||
	} as Channel);
 | 
			
		||||
	} as Channel).then(x => Channels.findOneOrFail(x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
	return await Channels.pack(channel, user);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import define from '../../../define';
 | 
			
		|||
import { apiLogger } from '../../../logger';
 | 
			
		||||
import { ApiError } from '../../../error';
 | 
			
		||||
import { DriveFiles } from '@/models/index';
 | 
			
		||||
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['drive'],
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +33,11 @@ export const meta = {
 | 
			
		|||
			default: null,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		comment: {
 | 
			
		||||
			validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
 | 
			
		||||
			default: null,
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		isSensitive: {
 | 
			
		||||
			validator: $.optional.either($.bool, $.str),
 | 
			
		||||
			default: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +85,7 @@ export default define(meta, async (ps, user, _, file, cleanup) => {
 | 
			
		|||
 | 
			
		||||
	try {
 | 
			
		||||
		// Create file
 | 
			
		||||
		const driveFile = await addFile(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive);
 | 
			
		||||
		const driveFile = await addFile({ user, path: file.path, name, comment: ps.comment, folderId: ps.folderId, force: ps.force, sensitive: ps.isSensitive });
 | 
			
		||||
		return await DriveFiles.pack(driveFile, { self: true });
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		apiLogger.error(e);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import $ from 'cafy';
 | 
			
		||||
import { ID } from '@/misc/cafy-id';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import uploadFromUrl from '@/services/drive/upload-from-url';
 | 
			
		||||
import { uploadFromUrl } from '@/services/drive/upload-from-url';
 | 
			
		||||
import define from '../../../define';
 | 
			
		||||
import { DriveFiles } from '@/models/index';
 | 
			
		||||
import { publishMainStream } from '@/services/stream';
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ export const meta = {
 | 
			
		|||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
export default define(meta, async (ps, user) => {
 | 
			
		||||
	uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => {
 | 
			
		||||
	uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment }).then(file => {
 | 
			
		||||
		DriveFiles.pack(file, { self: true }).then(packedFile => {
 | 
			
		||||
			publishMainStream(user.id, 'urlUploadFinished', {
 | 
			
		||||
				marker: ps.marker,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -130,7 +130,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	const credentialIdString = credentialId.toString('hex');
 | 
			
		||||
 | 
			
		||||
	await UserSecurityKeys.save({
 | 
			
		||||
	await UserSecurityKeys.insert({
 | 
			
		||||
		userId: user.id,
 | 
			
		||||
		id: credentialIdString,
 | 
			
		||||
		lastUsed: new Date(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ export default define(meta, async (ps, user) => {
 | 
			
		|||
 | 
			
		||||
	const challengeId = genId();
 | 
			
		||||
 | 
			
		||||
	await AttestationChallenges.save({
 | 
			
		||||
	await AttestationChallenges.insert({
 | 
			
		||||
		userId: user.id,
 | 
			
		||||
		id: challengeId,
 | 
			
		||||
		challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,8 @@
 | 
			
		|||
import * as Router from '@koa/router';
 | 
			
		||||
import config from '@/config/index';
 | 
			
		||||
import { fetchMeta } from '@/misc/fetch-meta';
 | 
			
		||||
import { Users } from '@/models/index';
 | 
			
		||||
// import User from '../models/user';
 | 
			
		||||
// import Note from '../models/note';
 | 
			
		||||
import { Users, Notes } from '@/models/index';
 | 
			
		||||
import { Not, IsNull, MoreThan } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,20 +18,21 @@ export const links = [/* (awaiting release) {
 | 
			
		|||
}];
 | 
			
		||||
 | 
			
		||||
const nodeinfo2 = async () => {
 | 
			
		||||
	const now = Date.now();
 | 
			
		||||
	const [
 | 
			
		||||
		meta,
 | 
			
		||||
		// total,
 | 
			
		||||
		// activeHalfyear,
 | 
			
		||||
		// activeMonth,
 | 
			
		||||
		// localPosts,
 | 
			
		||||
		// localComments
 | 
			
		||||
		total,
 | 
			
		||||
		activeHalfyear,
 | 
			
		||||
		activeMonth,
 | 
			
		||||
		localPosts,
 | 
			
		||||
		localComments,
 | 
			
		||||
	] = await Promise.all([
 | 
			
		||||
		fetchMeta(true),
 | 
			
		||||
		// User.count({ host: null }),
 | 
			
		||||
		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 15552000000) } }),
 | 
			
		||||
		// User.count({ host: null, updatedAt: { $gt: new Date(Date.now() - 2592000000) } }),
 | 
			
		||||
		// Note.count({ '_user.host': null, replyId: null }),
 | 
			
		||||
		// Note.count({ '_user.host': null, replyId: { $ne: null } })
 | 
			
		||||
		Users.count({ where: { host: null } }),
 | 
			
		||||
		Users.count({ where: { host: null, updatedAt: MoreThan(new Date(now - 15552000000)) } }),
 | 
			
		||||
		Users.count({ where: { host: null, updatedAt: MoreThan(new Date(now - 2592000000)) } }),
 | 
			
		||||
		Notes.count({ where: { userHost: null, replyId: null } }),
 | 
			
		||||
		Notes.count({ where: { userHost: null, replyId: Not(IsNull()) } }),
 | 
			
		||||
	]);
 | 
			
		||||
 | 
			
		||||
	const proxyAccount = meta.proxyAccountId ? await Users.pack(meta.proxyAccountId).catch(() => null) : null;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,9 +50,9 @@ const nodeinfo2 = async () => {
 | 
			
		|||
		},
 | 
			
		||||
		openRegistrations: !meta.disableRegistration,
 | 
			
		||||
		usage: {
 | 
			
		||||
			users: {}, // { total, activeHalfyear, activeMonth },
 | 
			
		||||
			// localPosts,
 | 
			
		||||
			// localComments
 | 
			
		||||
			users: { total, activeHalfyear, activeMonth },
 | 
			
		||||
			localPosts,
 | 
			
		||||
			localComments,
 | 
			
		||||
		},
 | 
			
		||||
		metadata: {
 | 
			
		||||
			nodeName: meta.name,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -297,33 +297,45 @@ async function deleteOldFile(user: IRemoteUser) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AddFileArgs = {
 | 
			
		||||
	/** User who wish to add file */
 | 
			
		||||
	user: { id: User['id']; host: User['host'] } | null;
 | 
			
		||||
	/** File path */
 | 
			
		||||
	path: string;
 | 
			
		||||
	/** Name */
 | 
			
		||||
	name?: string | null;
 | 
			
		||||
	/** Comment */
 | 
			
		||||
	comment?: string | null;
 | 
			
		||||
	/** Folder ID */
 | 
			
		||||
	folderId?: any;
 | 
			
		||||
	/** If set to true, forcibly upload the file even if there is a file with the same hash. */
 | 
			
		||||
	force?: boolean;
 | 
			
		||||
	/** Do not save file to local */
 | 
			
		||||
	isLink?: boolean;
 | 
			
		||||
	/** URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL) */
 | 
			
		||||
	url?: string | null;
 | 
			
		||||
	/** URL of source (リモートインスタンスのURLからアップロードされた場合の元URL) */
 | 
			
		||||
	uri?: string | null;
 | 
			
		||||
	/** Mark file as sensitive */
 | 
			
		||||
	sensitive?: boolean | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Add file to drive
 | 
			
		||||
 *
 | 
			
		||||
 * @param user User who wish to add file
 | 
			
		||||
 * @param path File path
 | 
			
		||||
 * @param name Name
 | 
			
		||||
 * @param comment Comment
 | 
			
		||||
 * @param folderId Folder ID
 | 
			
		||||
 * @param force If set to true, forcibly upload the file even if there is a file with the same hash.
 | 
			
		||||
 * @param isLink Do not save file to local
 | 
			
		||||
 * @param url URL of source (URLからアップロードされた場合(ローカル/リモート)の元URL)
 | 
			
		||||
 * @param uri URL of source (リモートインスタンスのURLからアップロードされた場合の元URL)
 | 
			
		||||
 * @param sensitive Mark file as sensitive
 | 
			
		||||
 * @return Created drive file
 | 
			
		||||
 */
 | 
			
		||||
export async function addFile(
 | 
			
		||||
	user: { id: User['id']; host: User['host'] } | null,
 | 
			
		||||
	path: string,
 | 
			
		||||
	name: string | null = null,
 | 
			
		||||
	comment: string | null = null,
 | 
			
		||||
	folderId: any = null,
 | 
			
		||||
	force: boolean = false,
 | 
			
		||||
	isLink: boolean = false,
 | 
			
		||||
	url: string | null = null,
 | 
			
		||||
	uri: string | null = null,
 | 
			
		||||
	sensitive: boolean | null = null
 | 
			
		||||
): Promise<DriveFile> {
 | 
			
		||||
export async function addFile({
 | 
			
		||||
	user,
 | 
			
		||||
	path,
 | 
			
		||||
	name = null,
 | 
			
		||||
	comment = null,
 | 
			
		||||
	folderId = null,
 | 
			
		||||
	force = false,
 | 
			
		||||
	isLink = false,
 | 
			
		||||
	url = null,
 | 
			
		||||
	uri = null,
 | 
			
		||||
	sensitive = null
 | 
			
		||||
}: AddFileArgs): Promise<DriveFile> {
 | 
			
		||||
	const info = await getFileInfo(path);
 | 
			
		||||
	logger.info(`${JSON.stringify(info)}`);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,16 +10,27 @@ import { DriveFiles } from '@/models/index';
 | 
			
		|||
 | 
			
		||||
const logger = driveLogger.createSubLogger('downloader');
 | 
			
		||||
 | 
			
		||||
export default async (
 | 
			
		||||
	url: string,
 | 
			
		||||
	user: { id: User['id']; host: User['host'] } | null,
 | 
			
		||||
	folderId: DriveFolder['id'] | null = null,
 | 
			
		||||
	uri: string | null = null,
 | 
			
		||||
type Args = {
 | 
			
		||||
	url: string;
 | 
			
		||||
	user: { id: User['id']; host: User['host'] } | null;
 | 
			
		||||
	folderId?: DriveFolder['id'] | null;
 | 
			
		||||
	uri?: string | null;
 | 
			
		||||
	sensitive?: boolean;
 | 
			
		||||
	force?: boolean;
 | 
			
		||||
	isLink?: boolean;
 | 
			
		||||
	comment?: string | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function uploadFromUrl({
 | 
			
		||||
	url,
 | 
			
		||||
	user,
 | 
			
		||||
	folderId = null,
 | 
			
		||||
	uri = null,
 | 
			
		||||
	sensitive = false,
 | 
			
		||||
	force = false,
 | 
			
		||||
	link = false,
 | 
			
		||||
	isLink = false,
 | 
			
		||||
	comment = null
 | 
			
		||||
): Promise<DriveFile> => {
 | 
			
		||||
}: Args): Promise<DriveFile> {
 | 
			
		||||
	let name = new URL(url).pathname.split('/').pop() || null;
 | 
			
		||||
	if (name == null || !DriveFiles.validateFileName(name)) {
 | 
			
		||||
		name = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +52,7 @@ export default async (
 | 
			
		|||
	let error;
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		driveFile = await addFile(user, path, name, comment, folderId, force, link, url, uri, sensitive);
 | 
			
		||||
		driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
 | 
			
		||||
		logger.succ(`Got: ${driveFile.id}`);
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		error = e;
 | 
			
		||||
| 
						 | 
				
			
			@ -59,4 +70,4 @@ export default async (
 | 
			
		|||
	} else {
 | 
			
		||||
		return driveFile!;
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
 | 
			
		|||
	if (blocking != null) throw new Error('blocking');
 | 
			
		||||
	if (blocked != null) throw new Error('blocked');
 | 
			
		||||
 | 
			
		||||
	const followRequest = await FollowRequests.save({
 | 
			
		||||
	const followRequest = await FollowRequests.insert({
 | 
			
		||||
		id: genId(),
 | 
			
		||||
		createdAt: new Date(),
 | 
			
		||||
		followerId: follower.id,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +39,7 @@ export default async function(follower: { id: User['id']; host: User['host']; ur
 | 
			
		|||
		followeeHost: followee.host,
 | 
			
		||||
		followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined,
 | 
			
		||||
		followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined,
 | 
			
		||||
	});
 | 
			
		||||
	}).then(x => FollowRequests.findOneOrFail(x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
	// Publish receiveRequest event
 | 
			
		||||
	if (Users.isLocalUser(followee)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,12 +16,12 @@ export async function registerOrFetchInstanceDoc(host: string): Promise<Instance
 | 
			
		|||
	const index = await Instances.findOne({ host });
 | 
			
		||||
 | 
			
		||||
	if (index == null) {
 | 
			
		||||
		const i = await Instances.save({
 | 
			
		||||
		const i = await Instances.insert({
 | 
			
		||||
			id: genId(),
 | 
			
		||||
			host,
 | 
			
		||||
			caughtAt: new Date(),
 | 
			
		||||
			lastCommunicatedAt: new Date(),
 | 
			
		||||
		});
 | 
			
		||||
		}).then(x => Instances.findOneOrFail(x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
		federationChart.update(true);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,7 @@ module.exports = {
 | 
			
		|||
		"vue/no-unused-components": "warn",
 | 
			
		||||
		"vue/valid-v-for": "warn",
 | 
			
		||||
		"vue/return-in-computed-property": "warn",
 | 
			
		||||
		"vue/no-setup-props-destructure": "warn",
 | 
			
		||||
		"vue/max-attributes-per-line": "off",
 | 
			
		||||
		"vue/html-self-closing": "off",
 | 
			
		||||
		"vue/singleline-html-element-content-newline": "off",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,7 +38,7 @@
 | 
			
		|||
		"@types/websocket": "1.0.4",
 | 
			
		||||
		"@types/ws": "8.2.2",
 | 
			
		||||
		"@typescript-eslint/parser": "5.10.0",
 | 
			
		||||
		"@vue/compiler-sfc": "3.2.28",
 | 
			
		||||
		"@vue/compiler-sfc": "3.2.29",
 | 
			
		||||
		"abort-controller": "3.0.0",
 | 
			
		||||
		"autobind-decorator": "2.4.0",
 | 
			
		||||
		"autosize": "5.0.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +112,7 @@
 | 
			
		|||
		"uuid": "8.3.2",
 | 
			
		||||
		"v-debounce": "0.1.2",
 | 
			
		||||
		"vanilla-tilt": "1.7.2",
 | 
			
		||||
		"vue": "3.2.28",
 | 
			
		||||
		"vue": "3.2.29",
 | 
			
		||||
		"vue-loader": "17.0.0",
 | 
			
		||||
		"vue-prism-editor": "2.0.0-alpha.2",
 | 
			
		||||
		"vue-router": "4.0.5",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -192,31 +192,31 @@ export async function openAccountMenu(opts: {
 | 
			
		|||
	if (opts.withExtraOperation) {
 | 
			
		||||
		popupMenu([...[{
 | 
			
		||||
			type: 'link',
 | 
			
		||||
			text: i18n.locale.profile,
 | 
			
		||||
			text: i18n.ts.profile,
 | 
			
		||||
			to: `/@${ $i.username }`,
 | 
			
		||||
			avatar: $i,
 | 
			
		||||
		}, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
 | 
			
		||||
			icon: 'fas fa-plus',
 | 
			
		||||
			text: i18n.locale.addAccount,
 | 
			
		||||
			text: i18n.ts.addAccount,
 | 
			
		||||
			action: () => {
 | 
			
		||||
				popupMenu([{
 | 
			
		||||
					text: i18n.locale.existingAccount,
 | 
			
		||||
					text: i18n.ts.existingAccount,
 | 
			
		||||
					action: () => { showSigninDialog(); },
 | 
			
		||||
				}, {
 | 
			
		||||
					text: i18n.locale.createAccount,
 | 
			
		||||
					text: i18n.ts.createAccount,
 | 
			
		||||
					action: () => { createAccount(); },
 | 
			
		||||
				}], ev.currentTarget || ev.target);
 | 
			
		||||
				}], ev.currentTarget ?? ev.target);
 | 
			
		||||
			},
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'link',
 | 
			
		||||
			icon: 'fas fa-users',
 | 
			
		||||
			text: i18n.locale.manageAccounts,
 | 
			
		||||
			text: i18n.ts.manageAccounts,
 | 
			
		||||
			to: `/settings/accounts`,
 | 
			
		||||
		}]], ev.currentTarget || ev.target, {
 | 
			
		||||
		}]], ev.currentTarget ?? ev.target, {
 | 
			
		||||
			align: 'left'
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, {
 | 
			
		||||
		popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
 | 
			
		||||
			align: 'left'
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,7 +2,7 @@
 | 
			
		|||
<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
 | 
			
		||||
		<I18n :src="i18n.locale.reportAbuseOf" tag="span">
 | 
			
		||||
		<I18n :src="i18n.ts.reportAbuseOf" tag="span">
 | 
			
		||||
			<template #name>
 | 
			
		||||
				<b><MkAcct :user="user"/></b>
 | 
			
		||||
			</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,12 +11,12 @@
 | 
			
		|||
	<div class="dpvffvvy _monolithic_">
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<MkTextarea v-model="comment">
 | 
			
		||||
				<template #label>{{ i18n.locale.details }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template>
 | 
			
		||||
				<template #label>{{ i18n.ts.details }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
 | 
			
		||||
			</MkTextarea>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton>
 | 
			
		||||
			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</XWindow>
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +50,7 @@ function send() {
 | 
			
		|||
	}, undefined).then(res => {
 | 
			
		||||
		os.alert({
 | 
			
		||||
			type: 'success',
 | 
			
		||||
			text: i18n.locale.abuseReported
 | 
			
		||||
			text: i18n.ts.abuseReported
 | 
			
		||||
		});
 | 
			
		||||
		window.value?.close();
 | 
			
		||||
		emit('closed');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
			</span>
 | 
			
		||||
			<span class="username">@{{ acct(user) }}</span>
 | 
			
		||||
		</li>
 | 
			
		||||
		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li>
 | 
			
		||||
		<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.ts.selectUser }}</li>
 | 
			
		||||
	</ol>
 | 
			
		||||
	<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
 | 
			
		||||
		<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span>
 | 
			
		||||
	<span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span>
 | 
			
		||||
	<div ref="captchaEl"></div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -38,7 +38,7 @@ const props = defineProps<{
 | 
			
		|||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'update:modelValue', v: string | null): void;
 | 
			
		||||
	(ev: 'update:modelValue', v: string | null): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const available = ref(false);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,14 +6,14 @@
 | 
			
		|||
>
 | 
			
		||||
	<template v-if="!wait">
 | 
			
		||||
		<template v-if="isFollowing">
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else>
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template v-else>
 | 
			
		||||
		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 | 
			
		||||
		<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 | 
			
		||||
	</template>
 | 
			
		||||
</button>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
		<div class="status">
 | 
			
		||||
			<div>
 | 
			
		||||
				<i class="fas fa-users fa-fw"></i>
 | 
			
		||||
				<I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;">
 | 
			
		||||
				<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
 | 
			
		||||
					<template #n>
 | 
			
		||||
						<b>{{ channel.usersCount }}</b>
 | 
			
		||||
					</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				<i class="fas fa-pencil-alt fa-fw"></i>
 | 
			
		||||
				<I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;">
 | 
			
		||||
				<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
 | 
			
		||||
					<template #n>
 | 
			
		||||
						<b>{{ channel.notesCount }}</b>
 | 
			
		||||
					</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +27,7 @@
 | 
			
		|||
	</article>
 | 
			
		||||
	<footer>
 | 
			
		||||
		<span v-if="channel.lastNotedAt">
 | 
			
		||||
			{{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
 | 
			
		||||
			{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
 | 
			
		||||
		</span>
 | 
			
		||||
	</footer>
 | 
			
		||||
</MkA>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,6 +143,7 @@ export default defineComponent({
 | 
			
		|||
			}
 | 
			
		||||
 | 
			
		||||
			const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 | 
			
		||||
			const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
			// フォントカラー
 | 
			
		||||
			Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
 | 
			
		||||
| 
						 | 
				
			
			@ -255,6 +256,27 @@ export default defineComponent({
 | 
			
		|||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				plugins: [{
 | 
			
		||||
					id: 'vLine',
 | 
			
		||||
					beforeDraw(chart, args, options) {
 | 
			
		||||
						if (chart.tooltip._active && chart.tooltip._active.length) {
 | 
			
		||||
							const activePoint = chart.tooltip._active[0];
 | 
			
		||||
							const ctx = chart.ctx;
 | 
			
		||||
							const x = activePoint.element.x;
 | 
			
		||||
							const topY = chart.scales.y.top;
 | 
			
		||||
							const bottomY = chart.scales.y.bottom;
 | 
			
		||||
 | 
			
		||||
							ctx.save();
 | 
			
		||||
							ctx.beginPath();
 | 
			
		||||
							ctx.moveTo(x, bottomY);
 | 
			
		||||
							ctx.lineTo(x, topY);
 | 
			
		||||
							ctx.lineWidth = 1;
 | 
			
		||||
							ctx.strokeStyle = vLineColor;
 | 
			
		||||
							ctx.stroke();
 | 
			
		||||
							ctx.restore();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}]
 | 
			
		||||
			});
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<button class="nrvgflfu _button" @click="toggle">
 | 
			
		||||
	<b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b>
 | 
			
		||||
	<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
 | 
			
		||||
	<span v-if="!modelValue">{{ label }}</span>
 | 
			
		||||
</button>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +25,7 @@ const label = computed(() => {
 | 
			
		|||
	return concat([
 | 
			
		||||
		props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
 | 
			
		||||
		props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [],
 | 
			
		||||
		props.note.poll != null ? [i18n.locale.poll] : []
 | 
			
		||||
		props.note.poll != null ? [i18n.ts.poll] : []
 | 
			
		||||
	] as string[][]).join(' / ');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,8 +28,8 @@
 | 
			
		|||
			</template>
 | 
			
		||||
		</MkSelect>
 | 
			
		||||
		<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
 | 
			
		||||
			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
 | 
			
		||||
			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
 | 
			
		||||
			<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
 | 
			
		||||
			<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="actions" class="buttons">
 | 
			
		||||
			<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		{{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
 | 
			
		||||
		{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
 | 
			
		||||
		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
 | 
			
		||||
	</template>
 | 
			
		||||
	<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@
 | 
			
		|||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>
 | 
			
		||||
		{{ i18n.locale.drive }}
 | 
			
		||||
		{{ i18n.ts.drive }}
 | 
			
		||||
	</template>
 | 
			
		||||
	<XDrive :initial-folder="initialFolder"/>
 | 
			
		||||
</XWindow>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,15 +10,15 @@
 | 
			
		|||
>
 | 
			
		||||
	<div v-if="$i?.avatarId == file.id" class="label">
 | 
			
		||||
		<img src="/client-assets/label.svg"/>
 | 
			
		||||
		<p>{{ i18n.locale.avatar }}</p>
 | 
			
		||||
		<p>{{ i18n.ts.avatar }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="$i?.bannerId == file.id" class="label">
 | 
			
		||||
		<img src="/client-assets/label.svg"/>
 | 
			
		||||
		<p>{{ i18n.locale.banner }}</p>
 | 
			
		||||
		<p>{{ i18n.ts.banner }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="file.isSensitive" class="label red">
 | 
			
		||||
		<img src="/client-assets/label-red.svg"/>
 | 
			
		||||
		<p>{{ i18n.locale.nsfw }}</p>
 | 
			
		||||
		<p>{{ i18n.ts.nsfw }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -61,30 +61,30 @@ const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(pro
 | 
			
		|||
 | 
			
		||||
function getMenu() {
 | 
			
		||||
	return [{
 | 
			
		||||
		text: i18n.locale.rename,
 | 
			
		||||
		text: i18n.ts.rename,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: rename
 | 
			
		||||
	}, {
 | 
			
		||||
		text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
 | 
			
		||||
		text: props.file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
 | 
			
		||||
		icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
 | 
			
		||||
		action: toggleSensitive
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.locale.describeFile,
 | 
			
		||||
		text: i18n.ts.describeFile,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: describe
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.copyUrl,
 | 
			
		||||
		text: i18n.ts.copyUrl,
 | 
			
		||||
		icon: 'fas fa-link',
 | 
			
		||||
		action: copyUrl
 | 
			
		||||
	}, {
 | 
			
		||||
		type: 'a',
 | 
			
		||||
		href: props.file.url,
 | 
			
		||||
		target: '_blank',
 | 
			
		||||
		text: i18n.locale.download,
 | 
			
		||||
		text: i18n.ts.download,
 | 
			
		||||
		icon: 'fas fa-download',
 | 
			
		||||
		download: props.file.name
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.delete,
 | 
			
		||||
		text: i18n.ts.delete,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		danger: true,
 | 
			
		||||
		action: deleteFile
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +95,7 @@ function onClick(ev: MouseEvent) {
 | 
			
		|||
	if (props.selectMode) {
 | 
			
		||||
		emit('chosen', props.file);
 | 
			
		||||
	} else {
 | 
			
		||||
		os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
 | 
			
		||||
		os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -120,8 +120,8 @@ function onDragend() {
 | 
			
		|||
 | 
			
		||||
function rename() {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.renameFile,
 | 
			
		||||
		placeholder: i18n.locale.inputNewFileName,
 | 
			
		||||
		title: i18n.ts.renameFile,
 | 
			
		||||
		placeholder: i18n.ts.inputNewFileName,
 | 
			
		||||
		default: props.file.name,
 | 
			
		||||
	}).then(({ canceled, result: name }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -134,9 +134,9 @@ function rename() {
 | 
			
		|||
 | 
			
		||||
function describe() {
 | 
			
		||||
	os.popup(import('@/components/media-caption.vue'), {
 | 
			
		||||
		title: i18n.locale.describeFile,
 | 
			
		||||
		title: i18n.ts.describeFile,
 | 
			
		||||
		input: {
 | 
			
		||||
			placeholder: i18n.locale.inputNewDescription,
 | 
			
		||||
			placeholder: i18n.ts.inputNewDescription,
 | 
			
		||||
			default: props.file.comment !== null ? props.file.comment : '',
 | 
			
		||||
		},
 | 
			
		||||
		image: props.file
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@
 | 
			
		|||
		{{ folder.name }}
 | 
			
		||||
	</p>
 | 
			
		||||
	<p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
 | 
			
		||||
		{{ i18n.locale.uploadFolder }}
 | 
			
		||||
		{{ i18n.ts.uploadFolder }}
 | 
			
		||||
	</p>
 | 
			
		||||
	<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -146,14 +146,14 @@ function onDrop(ev: DragEvent) {
 | 
			
		|||
			switch (err) {
 | 
			
		||||
				case 'detected-circular-definition':
 | 
			
		||||
					os.alert({
 | 
			
		||||
						title: i18n.locale.unableToProcess,
 | 
			
		||||
						text: i18n.locale.circularReferenceFolder
 | 
			
		||||
						title: i18n.ts.unableToProcess,
 | 
			
		||||
						text: i18n.ts.circularReferenceFolder
 | 
			
		||||
					});
 | 
			
		||||
					break;
 | 
			
		||||
				default:
 | 
			
		||||
					os.alert({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: i18n.locale.somethingHappened
 | 
			
		||||
						text: i18n.ts.somethingHappened
 | 
			
		||||
					});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			@ -184,8 +184,8 @@ function go() {
 | 
			
		|||
 | 
			
		||||
function rename() {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.renameFolder,
 | 
			
		||||
		placeholder: i18n.locale.inputNewFolderName,
 | 
			
		||||
		title: i18n.ts.renameFolder,
 | 
			
		||||
		placeholder: i18n.ts.inputNewFolderName,
 | 
			
		||||
		default: props.folder.name
 | 
			
		||||
	}).then(({ canceled, result: name }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -208,14 +208,14 @@ function deleteFolder() {
 | 
			
		|||
			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					title: i18n.locale.unableToDelete,
 | 
			
		||||
					text: i18n.locale.hasChildFilesOrFolders
 | 
			
		||||
					title: i18n.ts.unableToDelete,
 | 
			
		||||
					text: i18n.ts.hasChildFilesOrFolders
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: i18n.locale.unableToDelete
 | 
			
		||||
					text: i18n.ts.unableToDelete
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			@ -227,7 +227,7 @@ function setAsUploadFolder() {
 | 
			
		|||
 | 
			
		||||
function onContextmenu(ev: MouseEvent) {
 | 
			
		||||
	os.contextMenu([{
 | 
			
		||||
		text: i18n.locale.openInWindow,
 | 
			
		||||
		text: i18n.ts.openInWindow,
 | 
			
		||||
		icon: 'fas fa-window-restore',
 | 
			
		||||
		action: () => {
 | 
			
		||||
			os.popup(import('./drive-window.vue'), {
 | 
			
		||||
| 
						 | 
				
			
			@ -236,11 +236,11 @@ function onContextmenu(ev: MouseEvent) {
 | 
			
		|||
			}, 'closed');
 | 
			
		||||
		}
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.rename,
 | 
			
		||||
		text: i18n.ts.rename,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: rename,
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: i18n.locale.delete,
 | 
			
		||||
		text: i18n.ts.delete,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		danger: true,
 | 
			
		||||
		action: deleteFolder,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
	@drop.stop="onDrop"
 | 
			
		||||
>
 | 
			
		||||
	<i v-if="folder == null" class="fas fa-cloud"></i>
 | 
			
		||||
	<span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
 | 
			
		||||
	<span>{{ folder == null ? i18n.ts.drive : folder.name }}</span>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,7 @@
 | 
			
		|||
				/>
 | 
			
		||||
				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 | 
			
		||||
				<div v-for="(n, i) in 16" :key="i" class="padding"></div>
 | 
			
		||||
				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
 | 
			
		||||
				<MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.ts.loadMore }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-show="files.length > 0" ref="filesContainer" class="files">
 | 
			
		||||
				<XFile
 | 
			
		||||
| 
						 | 
				
			
			@ -71,12 +71,12 @@
 | 
			
		|||
				/>
 | 
			
		||||
				<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
 | 
			
		||||
				<div v-for="(n, i) in 16" :key="i" class="padding"></div>
 | 
			
		||||
				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
 | 
			
		||||
				<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
 | 
			
		||||
				<p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
 | 
			
		||||
				<p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
 | 
			
		||||
				<p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
 | 
			
		||||
				<p v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
 | 
			
		||||
				<p v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkLoading v-if="fetching"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -253,14 +253,14 @@ function onDrop(e: DragEvent): any {
 | 
			
		|||
			switch (err) {
 | 
			
		||||
				case 'detected-circular-definition':
 | 
			
		||||
					os.alert({
 | 
			
		||||
						title: i18n.locale.unableToProcess,
 | 
			
		||||
						text: i18n.locale.circularReferenceFolder
 | 
			
		||||
						title: i18n.ts.unableToProcess,
 | 
			
		||||
						text: i18n.ts.circularReferenceFolder
 | 
			
		||||
					});
 | 
			
		||||
					break;
 | 
			
		||||
				default:
 | 
			
		||||
					os.alert({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: i18n.locale.somethingHappened
 | 
			
		||||
						text: i18n.ts.somethingHappened
 | 
			
		||||
					});
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			@ -274,9 +274,9 @@ function selectLocalFile() {
 | 
			
		|||
 | 
			
		||||
function urlUpload() {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.uploadFromUrl,
 | 
			
		||||
		title: i18n.ts.uploadFromUrl,
 | 
			
		||||
		type: 'url',
 | 
			
		||||
		placeholder: i18n.locale.uploadFromUrlDescription
 | 
			
		||||
		placeholder: i18n.ts.uploadFromUrlDescription
 | 
			
		||||
	}).then(({ canceled, result: url }) => {
 | 
			
		||||
		if (canceled || !url) return;
 | 
			
		||||
		os.api('drive/files/upload-from-url', {
 | 
			
		||||
| 
						 | 
				
			
			@ -285,16 +285,16 @@ function urlUpload() {
 | 
			
		|||
		});
 | 
			
		||||
 | 
			
		||||
		os.alert({
 | 
			
		||||
			title: i18n.locale.uploadFromUrlRequested,
 | 
			
		||||
			text: i18n.locale.uploadFromUrlMayTakeTime
 | 
			
		||||
			title: i18n.ts.uploadFromUrlRequested,
 | 
			
		||||
			text: i18n.ts.uploadFromUrlMayTakeTime
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createFolder() {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.createFolder,
 | 
			
		||||
		placeholder: i18n.locale.folderName
 | 
			
		||||
		title: i18n.ts.createFolder,
 | 
			
		||||
		placeholder: i18n.ts.folderName
 | 
			
		||||
	}).then(({ canceled, result: name }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
		os.api('drive/folders/create', {
 | 
			
		||||
| 
						 | 
				
			
			@ -308,8 +308,8 @@ function createFolder() {
 | 
			
		|||
 | 
			
		||||
function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
 | 
			
		||||
	os.inputText({
 | 
			
		||||
		title: i18n.locale.renameFolder,
 | 
			
		||||
		placeholder: i18n.locale.inputNewFolderName,
 | 
			
		||||
		title: i18n.ts.renameFolder,
 | 
			
		||||
		placeholder: i18n.ts.inputNewFolderName,
 | 
			
		||||
		default: folderToRename.name
 | 
			
		||||
	}).then(({ canceled, result: name }) => {
 | 
			
		||||
		if (canceled) return;
 | 
			
		||||
| 
						 | 
				
			
			@ -334,14 +334,14 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
 | 
			
		|||
			case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					title: i18n.locale.unableToDelete,
 | 
			
		||||
					text: i18n.locale.hasChildFilesOrFolders
 | 
			
		||||
					title: i18n.ts.unableToDelete,
 | 
			
		||||
					text: i18n.ts.hasChildFilesOrFolders
 | 
			
		||||
				});
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: i18n.locale.unableToDelete
 | 
			
		||||
					text: i18n.ts.unableToDelete
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			@ -562,36 +562,36 @@ function fetchMoreFiles() {
 | 
			
		|||
 | 
			
		||||
function getMenu() {
 | 
			
		||||
	return [{
 | 
			
		||||
		text: i18n.locale.addFile,
 | 
			
		||||
		text: i18n.ts.addFile,
 | 
			
		||||
		type: 'label'
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.locale.upload,
 | 
			
		||||
		text: i18n.ts.upload,
 | 
			
		||||
		icon: 'fas fa-upload',
 | 
			
		||||
		action: () => { selectLocalFile(); }
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.locale.fromUrl,
 | 
			
		||||
		text: i18n.ts.fromUrl,
 | 
			
		||||
		icon: 'fas fa-link',
 | 
			
		||||
		action: () => { urlUpload(); }
 | 
			
		||||
	}, null, {
 | 
			
		||||
		text: folder.value ? folder.value.name : i18n.locale.drive,
 | 
			
		||||
		text: folder.value ? folder.value.name : i18n.ts.drive,
 | 
			
		||||
		type: 'label'
 | 
			
		||||
	}, folder.value ? {
 | 
			
		||||
		text: i18n.locale.renameFolder,
 | 
			
		||||
		text: i18n.ts.renameFolder,
 | 
			
		||||
		icon: 'fas fa-i-cursor',
 | 
			
		||||
		action: () => { renameFolder(folder.value); }
 | 
			
		||||
	} : undefined, folder.value ? {
 | 
			
		||||
		text: i18n.locale.deleteFolder,
 | 
			
		||||
		text: i18n.ts.deleteFolder,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
 | 
			
		||||
	} : undefined, {
 | 
			
		||||
		text: i18n.locale.createFolder,
 | 
			
		||||
		text: i18n.ts.createFolder,
 | 
			
		||||
		icon: 'fas fa-folder-plus',
 | 
			
		||||
		action: () => { createFolder(); }
 | 
			
		||||
	}];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showMenu(ev: MouseEvent) {
 | 
			
		||||
	os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
 | 
			
		||||
	os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onContextmenu(ev: MouseEvent) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,20 +32,20 @@ import MkEmojiPicker from '@/components/emoji-picker.vue';
 | 
			
		|||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
	manualShowing?: boolean;
 | 
			
		||||
	manualShowing?: boolean | null;
 | 
			
		||||
	src?: HTMLElement;
 | 
			
		||||
	showPinned?: boolean;
 | 
			
		||||
	asReactionPicker?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	manualShowing: false,
 | 
			
		||||
	manualShowing: null,
 | 
			
		||||
	showPinned: true,
 | 
			
		||||
	asReactionPicker: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'done', v: any): void;
 | 
			
		||||
	(e: 'close'): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'done', v: any): void;
 | 
			
		||||
	(ev: 'close'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const modal = ref<InstanceType<typeof MkModal>>();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
 | 
			
		||||
	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
 | 
			
		||||
	<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
 | 
			
		||||
	<div ref="emojis" class="emojis">
 | 
			
		||||
		<section class="result">
 | 
			
		||||
			<div v-if="searchResultCustom.length > 0">
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +43,7 @@
 | 
			
		|||
			</section>
 | 
			
		||||
 | 
			
		||||
			<section>
 | 
			
		||||
				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
 | 
			
		||||
				<header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.ts.recentUsed }}</header>
 | 
			
		||||
				<div>
 | 
			
		||||
					<button v-for="emoji in recentlyUsedEmojis"
 | 
			
		||||
						:key="emoji"
 | 
			
		||||
| 
						 | 
				
			
			@ -56,11 +56,11 @@
 | 
			
		|||
			</section>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
 | 
			
		||||
			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
 | 
			
		||||
			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
 | 
			
		||||
			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<header class="_acrylic">{{ i18n.locale.emoji }}</header>
 | 
			
		||||
			<header class="_acrylic">{{ i18n.ts.emoji }}</header>
 | 
			
		||||
			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -280,7 +280,7 @@ function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef):
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function chosen(emoji: any, ev?: MouseEvent) {
 | 
			
		||||
	const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
 | 
			
		||||
	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
 | 
			
		||||
	if (el) {
 | 
			
		||||
		const rect = el.getBoundingClientRect();
 | 
			
		||||
		const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,23 +6,23 @@
 | 
			
		|||
>
 | 
			
		||||
	<template v-if="!wait">
 | 
			
		||||
		<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else-if="isFollowing">
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.unfollow }}</span><i class="fas fa-minus"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else-if="!isFollowing && user.isLocked">
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.followRequest }}</span><i class="fas fa-plus"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
		<template v-else-if="!isFollowing && !user.isLocked">
 | 
			
		||||
			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
 | 
			
		||||
			<span v-if="full">{{ i18n.ts.follow }}</span><i class="fas fa-plus"></i>
 | 
			
		||||
		</template>
 | 
			
		||||
	</template>
 | 
			
		||||
	<template v-else>
 | 
			
		||||
		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 | 
			
		||||
		<span v-if="full">{{ i18n.ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
 | 
			
		||||
	</template>
 | 
			
		||||
</button>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,28 +5,28 @@
 | 
			
		|||
	@close="dialog.close()"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ i18n.locale.forgotPassword }}</template>
 | 
			
		||||
	<template #header>{{ i18n.ts.forgotPassword }}</template>
 | 
			
		||||
 | 
			
		||||
	<form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
 | 
			
		||||
		<div class="main _formRoot">
 | 
			
		||||
			<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
 | 
			
		||||
				<template #label>{{ i18n.locale.username }}</template>
 | 
			
		||||
				<template #label>{{ i18n.ts.username }}</template>
 | 
			
		||||
				<template #prefix>@</template>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
 | 
			
		||||
			<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
 | 
			
		||||
				<template #label>{{ i18n.locale.emailAddress }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
 | 
			
		||||
				<template #label>{{ i18n.ts.emailAddress }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.ts._forgotPassword.enterEmail }}</template>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
 | 
			
		||||
			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
 | 
			
		||||
			<MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="sub">
 | 
			
		||||
			<MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
 | 
			
		||||
			<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
 | 
			
		||||
		</div>
 | 
			
		||||
	</form>
 | 
			
		||||
	<div v-else class="bafecedb">
 | 
			
		||||
		{{ i18n.locale._forgotPassword.contactAdmin }}
 | 
			
		||||
		{{ i18n.ts._forgotPassword.contactAdmin }}
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
<transition name="fade" mode="out-in">
 | 
			
		||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 | 
			
		||||
	<div v-if="pending">
 | 
			
		||||
		<MkLoading/>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,31 +43,31 @@ function onContextmenu(ev) {
 | 
			
		|||
		text: props.to,
 | 
			
		||||
	}, {
 | 
			
		||||
		icon: 'fas fa-window-maximize',
 | 
			
		||||
		text: i18n.locale.openInWindow,
 | 
			
		||||
		text: i18n.ts.openInWindow,
 | 
			
		||||
		action: () => {
 | 
			
		||||
			os.pageWindow(props.to);
 | 
			
		||||
		}
 | 
			
		||||
	}, sideViewHook ? {
 | 
			
		||||
		icon: 'fas fa-columns',
 | 
			
		||||
		text: i18n.locale.openInSideView,
 | 
			
		||||
		text: i18n.ts.openInSideView,
 | 
			
		||||
		action: () => {
 | 
			
		||||
			sideViewHook(props.to);
 | 
			
		||||
		}
 | 
			
		||||
	} : undefined, {
 | 
			
		||||
		icon: 'fas fa-expand-alt',
 | 
			
		||||
		text: i18n.locale.showInPage,
 | 
			
		||||
		text: i18n.ts.showInPage,
 | 
			
		||||
		action: () => {
 | 
			
		||||
			router.push(props.to);
 | 
			
		||||
		}
 | 
			
		||||
	}, null, {
 | 
			
		||||
		icon: 'fas fa-external-link-alt',
 | 
			
		||||
		text: i18n.locale.openInNewTab,
 | 
			
		||||
		text: i18n.ts.openInNewTab,
 | 
			
		||||
		action: () => {
 | 
			
		||||
			window.open(props.to, '_blank');
 | 
			
		||||
		}
 | 
			
		||||
	}, {
 | 
			
		||||
		icon: 'fas fa-link',
 | 
			
		||||
		text: i18n.locale.copyLink,
 | 
			
		||||
		text: i18n.ts.copyLink,
 | 
			
		||||
		action: () => {
 | 
			
		||||
			copyToClipboard(`${url}${props.to}`);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -104,7 +104,7 @@ export default defineComponent({
 | 
			
		|||
			if (props.info.share) {
 | 
			
		||||
				if (menu.length > 0) menu.push(null);
 | 
			
		||||
				menu.push({
 | 
			
		||||
					text: i18n.locale.share,
 | 
			
		||||
					text: i18n.ts.share,
 | 
			
		||||
					icon: 'fas fa-share-alt',
 | 
			
		||||
					action: share
 | 
			
		||||
				});
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +113,7 @@ export default defineComponent({
 | 
			
		|||
				if (menu.length > 0) menu.push(null);
 | 
			
		||||
				menu = menu.concat(props.menu);
 | 
			
		||||
			}
 | 
			
		||||
			popupMenu(menu, ev.currentTarget || ev.target);
 | 
			
		||||
			popupMenu(menu, ev.currentTarget ?? ev.target);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const showTabsPopup = (ev: MouseEvent) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -126,7 +126,7 @@ export default defineComponent({
 | 
			
		|||
				icon: tab.icon,
 | 
			
		||||
				action: tab.onClick,
 | 
			
		||||
			}));
 | 
			
		||||
			popupMenu(menu, ev.currentTarget || ev.target);
 | 
			
		||||
			popupMenu(menu, ev.currentTarget ?? ev.target);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const preventDrag = (ev: TouchEvent) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,16 +24,16 @@ 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 >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: Math.round(ago / 31536000).toString() }) :
 | 
			
		||||
		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: Math.round(ago / 2592000).toString() }) :
 | 
			
		||||
		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: Math.round(ago / 604800).toString() }) :
 | 
			
		||||
		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: Math.round(ago / 86400).toString() }) :
 | 
			
		||||
		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: Math.round(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);
 | 
			
		||||
		ago >= -1       ? i18n.ts._ago.justNow :
 | 
			
		||||
		ago <  -1       ? i18n.ts._ago.future :
 | 
			
		||||
		i18n.ts._ago.unknown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function tick() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,52 +20,32 @@
 | 
			
		|||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { watch } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 | 
			
		||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		ImgWithBlurhash
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		image: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		raw: {
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			hide: true,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		url(): any {
 | 
			
		||||
			let url = this.$store.state.disableShowingAnimatedImages
 | 
			
		||||
				? getStaticImageUrl(this.image.thumbnailUrl)
 | 
			
		||||
				: this.image.thumbnailUrl;
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	image: misskey.entities.DriveFile;
 | 
			
		||||
	raw?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
			if (this.raw || this.$store.state.loadRawImages) {
 | 
			
		||||
				url = this.image.url;
 | 
			
		||||
			}
 | 
			
		||||
let hide = $ref(true);
 | 
			
		||||
 | 
			
		||||
			return url;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
 | 
			
		||||
		this.$watch('image', () => {
 | 
			
		||||
			this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
 | 
			
		||||
		}, {
 | 
			
		||||
			deep: true,
 | 
			
		||||
			immediate: true,
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
const url = (props.raw || defaultStore.state.loadRawImages)
 | 
			
		||||
	? props.image.url
 | 
			
		||||
	: defaultStore.state.disableShowingAnimatedImages
 | 
			
		||||
			? getStaticImageUrl(props.image.thumbnailUrl)
 | 
			
		||||
			: props.image.thumbnailUrl;
 | 
			
		||||
 | 
			
		||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
 | 
			
		||||
watch(() => props.image, () => {
 | 
			
		||||
	hide = (defaultStore.state.nsfw === 'force') ? true : props.image.isSensitive && (defaultStore.state.nsfw !== 'ignore');
 | 
			
		||||
}, {
 | 
			
		||||
	deep: true,
 | 
			
		||||
	immediate: true,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
	<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
 | 
			
		||||
	<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
 | 
			
		||||
		<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
 | 
			
		||||
			<template v-for="media in mediaList">
 | 
			
		||||
			<template v-for="media in mediaList.filter(media => previewable(media))">
 | 
			
		||||
				<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
 | 
			
		||||
				<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
 | 
			
		||||
			</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -12,8 +12,8 @@
 | 
			
		|||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, onMounted, PropType, ref } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, ref } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import PhotoSwipeLightbox from 'photoswipe/dist/photoswipe-lightbox.esm.js';
 | 
			
		||||
import PhotoSwipe from 'photoswipe/dist/photoswipe.esm.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -22,93 +22,83 @@ import XBanner from './media-banner.vue';
 | 
			
		|||
import XImage from './media-image.vue';
 | 
			
		||||
import XVideo from './media-video.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { FILE_TYPE_BROWSERSAFE } from '@/const';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XBanner,
 | 
			
		||||
		XImage,
 | 
			
		||||
		XVideo,
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		mediaList: {
 | 
			
		||||
			type: Array as PropType<misskey.entities.DriveFile[]>,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
		raw: {
 | 
			
		||||
			default: false
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	setup(props) {
 | 
			
		||||
		const gallery = ref(null);
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	mediaList: misskey.entities.DriveFile[];
 | 
			
		||||
	raw?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
		onMounted(() => {
 | 
			
		||||
			const lightbox = new PhotoSwipeLightbox({
 | 
			
		||||
				dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
 | 
			
		||||
					const item = {
 | 
			
		||||
						src: media.url,
 | 
			
		||||
						w: media.properties.width,
 | 
			
		||||
						h: media.properties.height,
 | 
			
		||||
						alt: media.name,
 | 
			
		||||
					};
 | 
			
		||||
					if (media.properties.orientation != null && media.properties.orientation >= 5) {
 | 
			
		||||
						[item.w, item.h] = [item.h, item.w];
 | 
			
		||||
					}
 | 
			
		||||
					return item;
 | 
			
		||||
				}),
 | 
			
		||||
				gallery: gallery.value,
 | 
			
		||||
				children: '.image',
 | 
			
		||||
				thumbSelector: '.image',
 | 
			
		||||
				loop: false,
 | 
			
		||||
				padding: window.innerWidth > 500 ? {
 | 
			
		||||
					top: 32,
 | 
			
		||||
					bottom: 32,
 | 
			
		||||
					left: 32,
 | 
			
		||||
					right: 32,
 | 
			
		||||
				} : {
 | 
			
		||||
					top: 0,
 | 
			
		||||
					bottom: 0,
 | 
			
		||||
					left: 0,
 | 
			
		||||
					right: 0,
 | 
			
		||||
				},
 | 
			
		||||
				imageClickAction: 'close',
 | 
			
		||||
				tapAction: 'toggle-controls',
 | 
			
		||||
				pswpModule: PhotoSwipe,
 | 
			
		||||
			});
 | 
			
		||||
const gallery = ref(null);
 | 
			
		||||
const pswpZIndex = os.claimZIndex('middle');
 | 
			
		||||
 | 
			
		||||
			lightbox.on('itemData', (e) => {
 | 
			
		||||
				const { itemData } = e;
 | 
			
		||||
 | 
			
		||||
				// element is children
 | 
			
		||||
				const { element } = itemData;
 | 
			
		||||
 | 
			
		||||
				const id = element.dataset.id;
 | 
			
		||||
				const file = props.mediaList.find(media => media.id === id);
 | 
			
		||||
 | 
			
		||||
				itemData.src = file.url;
 | 
			
		||||
				itemData.w = Number(file.properties.width);
 | 
			
		||||
				itemData.h = Number(file.properties.height);
 | 
			
		||||
				if (file.properties.orientation != null && file.properties.orientation >= 5) {
 | 
			
		||||
					[itemData.w, itemData.h] = [itemData.h, itemData.w];
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const lightbox = new PhotoSwipeLightbox({
 | 
			
		||||
		dataSource: props.mediaList
 | 
			
		||||
			.filter(media => {
 | 
			
		||||
				if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
 | 
			
		||||
				return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
 | 
			
		||||
			})
 | 
			
		||||
			.map(media => {
 | 
			
		||||
				const item = {
 | 
			
		||||
					src: media.url,
 | 
			
		||||
					w: media.properties.width,
 | 
			
		||||
					h: media.properties.height,
 | 
			
		||||
					alt: media.name,
 | 
			
		||||
				};
 | 
			
		||||
				if (media.properties.orientation != null && media.properties.orientation >= 5) {
 | 
			
		||||
					[item.w, item.h] = [item.h, item.w];
 | 
			
		||||
				}
 | 
			
		||||
				itemData.msrc = file.thumbnailUrl;
 | 
			
		||||
				itemData.thumbCropped = true;
 | 
			
		||||
			});
 | 
			
		||||
				return item;
 | 
			
		||||
			}),
 | 
			
		||||
		gallery: gallery.value,
 | 
			
		||||
		children: '.image',
 | 
			
		||||
		thumbSelector: '.image',
 | 
			
		||||
		loop: false,
 | 
			
		||||
		padding: window.innerWidth > 500 ? {
 | 
			
		||||
			top: 32,
 | 
			
		||||
			bottom: 32,
 | 
			
		||||
			left: 32,
 | 
			
		||||
			right: 32,
 | 
			
		||||
		} : {
 | 
			
		||||
			top: 0,
 | 
			
		||||
			bottom: 0,
 | 
			
		||||
			left: 0,
 | 
			
		||||
			right: 0,
 | 
			
		||||
		},
 | 
			
		||||
		imageClickAction: 'close',
 | 
			
		||||
		tapAction: 'toggle-controls',
 | 
			
		||||
		pswpModule: PhotoSwipe,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
			lightbox.init();
 | 
			
		||||
		});
 | 
			
		||||
	lightbox.on('itemData', (ev) => {
 | 
			
		||||
		const { itemData } = ev;
 | 
			
		||||
 | 
			
		||||
		const previewable = (file: misskey.entities.DriveFile): boolean => {
 | 
			
		||||
			return file.type.startsWith('video') || file.type.startsWith('image');
 | 
			
		||||
		};
 | 
			
		||||
		// element is children
 | 
			
		||||
		const { element } = itemData;
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			previewable,
 | 
			
		||||
			gallery,
 | 
			
		||||
			pswpZIndex: os.claimZIndex('middle'),
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
		const id = element.dataset.id;
 | 
			
		||||
		const file = props.mediaList.find(media => media.id === id);
 | 
			
		||||
 | 
			
		||||
		itemData.src = file.url;
 | 
			
		||||
		itemData.w = Number(file.properties.width);
 | 
			
		||||
		itemData.h = Number(file.properties.height);
 | 
			
		||||
		if (file.properties.orientation != null && file.properties.orientation >= 5) {
 | 
			
		||||
			[itemData.w, itemData.h] = [itemData.h, itemData.w];
 | 
			
		||||
		}
 | 
			
		||||
		itemData.msrc = file.thumbnailUrl;
 | 
			
		||||
		itemData.thumbCropped = true;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	lightbox.init();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const previewable = (file: misskey.entities.DriveFile): boolean => {
 | 
			
		||||
	if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
 | 
			
		||||
	// FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
 | 
			
		||||
	return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -250,7 +250,7 @@ function menu(viaKeyboard = false): void {
 | 
			
		|||
function showRenoteMenu(viaKeyboard = false): void {
 | 
			
		||||
	if (!isMyRenote) return;
 | 
			
		||||
	os.popupMenu([{
 | 
			
		||||
		text: i18n.locale.unrenote,
 | 
			
		||||
		text: i18n.ts.unrenote,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		danger: true,
 | 
			
		||||
		action: () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,13 +10,13 @@
 | 
			
		|||
	:class="{ renote: isRenote }"
 | 
			
		||||
>
 | 
			
		||||
	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
 | 
			
		||||
	<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> {{ 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> {{ i18n.locale.featured }}</div>
 | 
			
		||||
	<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
 | 
			
		||||
	<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
 | 
			
		||||
	<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
 | 
			
		||||
	<div v-if="isRenote" class="renote">
 | 
			
		||||
		<MkAvatar class="avatar" :user="note.user"/>
 | 
			
		||||
		<i class="fas fa-retweet"></i>
 | 
			
		||||
		<I18n :src="i18n.locale.renotedBy" tag="span">
 | 
			
		||||
		<I18n :src="i18n.ts.renotedBy" tag="span">
 | 
			
		||||
			<template #user>
 | 
			
		||||
				<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
 | 
			
		||||
					<MkUserName :user="note.user"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +48,7 @@
 | 
			
		|||
				</p>
 | 
			
		||||
				<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
 | 
			
		||||
					<div class="text">
 | 
			
		||||
						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span>
 | 
			
		||||
						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
 | 
			
		||||
						<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"/>
 | 
			
		||||
						<a v-if="appearNote.renote != null" class="rp">RN:</a>
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +67,7 @@
 | 
			
		|||
					<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>
 | 
			
		||||
					<button v-if="collapsed" class="fade _button" @click="collapsed = false">
 | 
			
		||||
						<span>{{ i18n.locale.showMore }}</span>
 | 
			
		||||
						<span>{{ i18n.ts.showMore }}</span>
 | 
			
		||||
					</button>
 | 
			
		||||
				</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>
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +94,7 @@
 | 
			
		|||
	</article>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else class="muted" @click="muted = false">
 | 
			
		||||
	<I18n :src="i18n.locale.userSaysSomething" tag="small">
 | 
			
		||||
	<I18n :src="i18n.ts.userSaysSomething" tag="small">
 | 
			
		||||
		<template #name>
 | 
			
		||||
			<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
 | 
			
		||||
				<MkUserName :user="appearNote.user"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -238,7 +238,7 @@ function menu(viaKeyboard = false): void {
 | 
			
		|||
function showRenoteMenu(viaKeyboard = false): void {
 | 
			
		||||
	if (!isMyRenote) return;
 | 
			
		||||
	os.popupMenu([{
 | 
			
		||||
		text: i18n.locale.unrenote,
 | 
			
		||||
		text: i18n.ts.unrenote,
 | 
			
		||||
		icon: 'fas fa-trash-alt',
 | 
			
		||||
		danger: true,
 | 
			
		||||
		action: () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mk-notification-toast" :style="{ zIndex }">
 | 
			
		||||
	<transition name="notification-toast" appear @after-leave="$emit('closed')">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'notification-toast' : ''" appear @after-leave="$emit('closed')">
 | 
			
		||||
		<XNotification v-if="showing" :notification="notification" class="notification _acrylic"/>
 | 
			
		||||
	</transition>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -160,7 +160,7 @@ export default defineComponent({
 | 
			
		|||
				action: () => {
 | 
			
		||||
					copyToClipboard(this.url);
 | 
			
		||||
				}
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
			}], ev.currentTarget ?? ev.target);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		back() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
	<p v-if="choices.length < 2" class="caution">
 | 
			
		||||
		<i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }}
 | 
			
		||||
	</p>
 | 
			
		||||
	<ul ref="choices">
 | 
			
		||||
	<ul>
 | 
			
		||||
		<li v-for="(choice, i) in choices" :key="i">
 | 
			
		||||
			<MkInput class="input" :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
 | 
			
		||||
			</MkInput>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,8 +14,8 @@
 | 
			
		|||
	</ul>
 | 
			
		||||
	<MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton>
 | 
			
		||||
	<MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton>
 | 
			
		||||
	<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
 | 
			
		||||
	<section>
 | 
			
		||||
		<MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
 | 
			
		||||
		<div>
 | 
			
		||||
			<MkSelect v-model="expiration">
 | 
			
		||||
				<template #label>{{ $ts._poll.expiration }}</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +31,7 @@
 | 
			
		|||
					<template #label>{{ $ts._poll.deadlineTime }}</template>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</section>
 | 
			
		||||
			<section v-if="expiration === 'after'">
 | 
			
		||||
			<section v-else-if="expiration === 'after'">
 | 
			
		||||
				<MkInput v-model="after" type="number" class="input">
 | 
			
		||||
					<template #label>{{ $ts._poll.duration }}</template>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
| 
						 | 
				
			
			@ -47,8 +47,8 @@
 | 
			
		|||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import { addTime } from '@/scripts/time';
 | 
			
		||||
import { formatDateTimeString } from '@/scripts/format-time-string';
 | 
			
		||||
import MkInput from './form/input.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -56,131 +56,91 @@ import MkSelect from './form/select.vue';
 | 
			
		|||
import MkSwitch from './form/switch.vue';
 | 
			
		||||
import MkButton from './ui/button.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkButton,
 | 
			
		||||
	},
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	modelValue: {
 | 
			
		||||
		expiresAt: string;
 | 
			
		||||
		expiredAfter: number;
 | 
			
		||||
		choices: string[];
 | 
			
		||||
		multiple: boolean;
 | 
			
		||||
	};
 | 
			
		||||
}>();
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'update:modelValue', v: {
 | 
			
		||||
		expiresAt: string;
 | 
			
		||||
		expiredAfter: number;
 | 
			
		||||
		choices: string[];
 | 
			
		||||
		multiple: boolean;
 | 
			
		||||
	}): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		poll: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true
 | 
			
		||||
const choices = ref(props.modelValue.choices);
 | 
			
		||||
const multiple = ref(props.modelValue.multiple);
 | 
			
		||||
const expiration = ref('infinite');
 | 
			
		||||
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
 | 
			
		||||
const atTime = ref('00:00');
 | 
			
		||||
const after = ref(0);
 | 
			
		||||
const unit = ref('second');
 | 
			
		||||
 | 
			
		||||
if (props.modelValue.expiresAt) {
 | 
			
		||||
	expiration.value = 'at';
 | 
			
		||||
	atDate.value = atTime.value = props.modelValue.expiresAt;
 | 
			
		||||
} else if (typeof props.modelValue.expiredAfter === 'number') {
 | 
			
		||||
	expiration.value = 'after';
 | 
			
		||||
	after.value = props.modelValue.expiredAfter / 1000;
 | 
			
		||||
} else {
 | 
			
		||||
	expiration.value = 'infinite';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onInput(i, value) {
 | 
			
		||||
	choices.value[i] = value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function add() {
 | 
			
		||||
	choices.value.push('');
 | 
			
		||||
	// TODO
 | 
			
		||||
	// nextTick(() => {
 | 
			
		||||
	//   (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
 | 
			
		||||
	// });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function remove(i) {
 | 
			
		||||
	choices.value = choices.value.filter((_, _i) => _i != i);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get() {
 | 
			
		||||
	const calcAt = () => {
 | 
			
		||||
		return new Date(`${atDate.value} ${atTime.value}`).getTime();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const calcAfter = () => {
 | 
			
		||||
		let base = parseInt(after.value);
 | 
			
		||||
		switch (unit.value) {
 | 
			
		||||
			case 'day': base *= 24;
 | 
			
		||||
			case 'hour': base *= 60;
 | 
			
		||||
			case 'minute': base *= 60;
 | 
			
		||||
			case 'second': return base *= 1000;
 | 
			
		||||
			default: return null;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	emits: ['updated'],
 | 
			
		||||
	return {
 | 
			
		||||
		choices: choices.value,
 | 
			
		||||
		multiple: multiple.value,
 | 
			
		||||
		...(
 | 
			
		||||
			expiration.value === 'at' ? { expiresAt: calcAt() } :
 | 
			
		||||
			expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
 | 
			
		||||
		)
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			choices: this.poll.choices,
 | 
			
		||||
			multiple: this.poll.multiple,
 | 
			
		||||
			expiration: 'infinite',
 | 
			
		||||
			atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
 | 
			
		||||
			atTime: '00:00',
 | 
			
		||||
			after: 0,
 | 
			
		||||
			unit: 'second',
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	watch: {
 | 
			
		||||
		choices: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.$emit('updated', this.get());
 | 
			
		||||
			},
 | 
			
		||||
			deep: true
 | 
			
		||||
		},
 | 
			
		||||
		multiple: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.$emit('updated', this.get());
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		expiration: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.$emit('updated', this.get());
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		atDate: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.$emit('updated', this.get());
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		after: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.$emit('updated', this.get());
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		unit: {
 | 
			
		||||
			handler() {
 | 
			
		||||
				this.$emit('updated', this.get());
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		const poll = this.poll;
 | 
			
		||||
		if (poll.expiresAt) {
 | 
			
		||||
			this.expiration = 'at';
 | 
			
		||||
			this.atDate = this.atTime = poll.expiresAt;
 | 
			
		||||
		} else if (typeof poll.expiredAfter === 'number') {
 | 
			
		||||
			this.expiration = 'after';
 | 
			
		||||
			this.after = poll.expiredAfter / 1000;
 | 
			
		||||
		} else {
 | 
			
		||||
			this.expiration = 'infinite';
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		onInput(i, e) {
 | 
			
		||||
			this.choices[i] = e;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		add() {
 | 
			
		||||
			this.choices.push('');
 | 
			
		||||
			this.$nextTick(() => {
 | 
			
		||||
				// TODO
 | 
			
		||||
				//(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		remove(i) {
 | 
			
		||||
			this.choices = this.choices.filter((_, _i) => _i != i);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		get() {
 | 
			
		||||
			const at = () => {
 | 
			
		||||
				return new Date(`${this.atDate} ${this.atTime}`).getTime();
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const after = () => {
 | 
			
		||||
				let base = parseInt(this.after);
 | 
			
		||||
				switch (this.unit) {
 | 
			
		||||
					case 'day': base *= 24;
 | 
			
		||||
					case 'hour': base *= 60;
 | 
			
		||||
					case 'minute': base *= 60;
 | 
			
		||||
					case 'second': return base *= 1000;
 | 
			
		||||
					default: return null;
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			return {
 | 
			
		||||
				choices: this.choices,
 | 
			
		||||
				multiple: this.multiple,
 | 
			
		||||
				...(
 | 
			
		||||
					this.expiration === 'at' ? { expiresAt: at() } :
 | 
			
		||||
					this.expiration === 'after' ? { expiredAfter: after() } : {}
 | 
			
		||||
				)
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
 | 
			
		||||
	deep: true,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.zmdxowus {
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
	padding: 8px 16px;
 | 
			
		||||
 | 
			
		||||
	> .caution {
 | 
			
		||||
		margin: 0 0 8px 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -216,7 +176,7 @@ export default defineComponent({
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	> .add {
 | 
			
		||||
		margin: 8px 0 0 0;
 | 
			
		||||
		margin: 8px 0;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -225,21 +185,27 @@ export default defineComponent({
 | 
			
		|||
 | 
			
		||||
		> div {
 | 
			
		||||
			margin: 0 8px;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			flex-direction: row;
 | 
			
		||||
			flex-wrap: wrap;
 | 
			
		||||
			gap: 12px;
 | 
			
		||||
 | 
			
		||||
			&:last-child {
 | 
			
		||||
				flex: 1 0 auto;
 | 
			
		||||
 | 
			
		||||
				> section {
 | 
			
		||||
					align-items: center;
 | 
			
		||||
					display: flex;
 | 
			
		||||
					margin: -32px 0 0;
 | 
			
		||||
				> div {
 | 
			
		||||
					flex-grow: 1;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
					> &:first-child {
 | 
			
		||||
						margin-right: 16px;
 | 
			
		||||
					}
 | 
			
		||||
				> section {
 | 
			
		||||
					// MAGIC: Prevent div above from growing unless wrapped to its own line
 | 
			
		||||
					flex-grow: 9999;
 | 
			
		||||
					align-items: end;
 | 
			
		||||
					display: flex;
 | 
			
		||||
					gap: 4px;
 | 
			
		||||
 | 
			
		||||
					> .input {
 | 
			
		||||
						flex: 1 0 auto;
 | 
			
		||||
						flex: 1 1 auto;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,7 +127,7 @@ export default defineComponent({
 | 
			
		|||
				text: this.$ts.attachCancel,
 | 
			
		||||
				icon: 'fas fa-times-circle',
 | 
			
		||||
				action: () => { this.detachMedia(file.id) }
 | 
			
		||||
			}], ev.currentTarget || ev.target).then(() => this.menu = null);
 | 
			
		||||
			}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,28 +8,28 @@
 | 
			
		|||
>
 | 
			
		||||
	<header>
 | 
			
		||||
		<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
 | 
			
		||||
		<button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu">
 | 
			
		||||
		<button v-click-anime v-tooltip="i18n.ts.switchAccount" class="account _button" @click="openAccountMenu">
 | 
			
		||||
			<MkAvatar :user="postAccount ?? $i" class="avatar"/>
 | 
			
		||||
		</button>
 | 
			
		||||
		<div>
 | 
			
		||||
			<span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
 | 
			
		||||
			<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
 | 
			
		||||
			<button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
 | 
			
		||||
			<button ref="visibilityButton" v-tooltip="i18n.ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
 | 
			
		||||
				<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
 | 
			
		||||
				<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
 | 
			
		||||
				<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
 | 
			
		||||
				<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
 | 
			
		||||
			</button>
 | 
			
		||||
			<button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
 | 
			
		||||
			<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</header>
 | 
			
		||||
	<div class="form" :class="{ fixed }">
 | 
			
		||||
		<XNoteSimple v-if="reply" class="preview" :note="reply"/>
 | 
			
		||||
		<XNoteSimple v-if="renote" class="preview" :note="renote"/>
 | 
			
		||||
		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
 | 
			
		||||
		<div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
 | 
			
		||||
		<div v-if="visibility === 'specified'" class="to-specified">
 | 
			
		||||
			<span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span>
 | 
			
		||||
			<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
 | 
			
		||||
			<div class="visibleUsers">
 | 
			
		||||
				<span v-for="u in visibleUsers" :key="u.id">
 | 
			
		||||
					<MkAcct :user="u"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -38,21 +38,21 @@
 | 
			
		|||
				<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo>
 | 
			
		||||
		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown">
 | 
			
		||||
		<MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
 | 
			
		||||
		<input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown">
 | 
			
		||||
		<textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
 | 
			
		||||
		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.hashtags" list="hashtags">
 | 
			
		||||
		<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
 | 
			
		||||
		<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
 | 
			
		||||
		<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
 | 
			
		||||
		<XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
 | 
			
		||||
		<XNotePreview v-if="showPreview" class="preview" :text="text"/>
 | 
			
		||||
		<footer>
 | 
			
		||||
			<button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
 | 
			
		||||
			<button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
 | 
			
		||||
			<button v-tooltip="i18n.ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
 | 
			
		||||
			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
 | 
			
		||||
		</footer>
 | 
			
		||||
		<datalist id="hashtags">
 | 
			
		||||
			<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -102,7 +102,7 @@ const props = withDefaults(defineProps<{
 | 
			
		|||
	initialLocalOnly?: boolean;
 | 
			
		||||
	initialVisibleUsers?: misskey.entities.User[];
 | 
			
		||||
	initialNote?: misskey.entities.Note;
 | 
			
		||||
	share?: boolean;
 | 
			
		||||
	instant?: boolean;
 | 
			
		||||
	fixed?: boolean;
 | 
			
		||||
	autofocus?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,9 +111,9 @@ const props = withDefaults(defineProps<{
 | 
			
		|||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'posted'): void;
 | 
			
		||||
	(e: 'cancel'): void;
 | 
			
		||||
	(e: 'esc'): void;
 | 
			
		||||
	(ev: 'posted'): void;
 | 
			
		||||
	(ev: 'cancel'): void;
 | 
			
		||||
	(ev: 'esc'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const textareaEl = $ref<HTMLTextAreaElement | null>(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -127,8 +127,8 @@ let files = $ref(props.initialFiles ?? []);
 | 
			
		|||
let poll = $ref<{
 | 
			
		||||
	choices: string[];
 | 
			
		||||
	multiple: boolean;
 | 
			
		||||
	expiresAt: string;
 | 
			
		||||
	expiredAfter: string;
 | 
			
		||||
	expiresAt: string | null;
 | 
			
		||||
	expiredAfter: string | null;
 | 
			
		||||
} | null>(null);
 | 
			
		||||
let useCw = $ref(false);
 | 
			
		||||
let showPreview = $ref(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -165,19 +165,19 @@ const draftKey = $computed((): string => {
 | 
			
		|||
 | 
			
		||||
const placeholder = $computed((): string => {
 | 
			
		||||
	if (props.renote) {
 | 
			
		||||
		return i18n.locale._postForm.quotePlaceholder;
 | 
			
		||||
		return i18n.ts._postForm.quotePlaceholder;
 | 
			
		||||
	} else if (props.reply) {
 | 
			
		||||
		return i18n.locale._postForm.replyPlaceholder;
 | 
			
		||||
		return i18n.ts._postForm.replyPlaceholder;
 | 
			
		||||
	} else if (props.channel) {
 | 
			
		||||
		return i18n.locale._postForm.channelPlaceholder;
 | 
			
		||||
		return i18n.ts._postForm.channelPlaceholder;
 | 
			
		||||
	} else {
 | 
			
		||||
		const xs = [
 | 
			
		||||
			i18n.locale._postForm._placeholders.a,
 | 
			
		||||
			i18n.locale._postForm._placeholders.b,
 | 
			
		||||
			i18n.locale._postForm._placeholders.c,
 | 
			
		||||
			i18n.locale._postForm._placeholders.d,
 | 
			
		||||
			i18n.locale._postForm._placeholders.e,
 | 
			
		||||
			i18n.locale._postForm._placeholders.f
 | 
			
		||||
			i18n.ts._postForm._placeholders.a,
 | 
			
		||||
			i18n.ts._postForm._placeholders.b,
 | 
			
		||||
			i18n.ts._postForm._placeholders.c,
 | 
			
		||||
			i18n.ts._postForm._placeholders.d,
 | 
			
		||||
			i18n.ts._postForm._placeholders.e,
 | 
			
		||||
			i18n.ts._postForm._placeholders.f
 | 
			
		||||
		];
 | 
			
		||||
		return xs[Math.floor(Math.random() * xs.length)];
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -185,10 +185,10 @@ const placeholder = $computed((): string => {
 | 
			
		|||
 | 
			
		||||
const submitText = $computed((): string => {
 | 
			
		||||
	return props.renote
 | 
			
		||||
		? i18n.locale.quote
 | 
			
		||||
		? i18n.ts.quote
 | 
			
		||||
		: props.reply
 | 
			
		||||
			? i18n.locale.reply
 | 
			
		||||
			: i18n.locale.note;
 | 
			
		||||
			? i18n.ts.reply
 | 
			
		||||
			: i18n.ts.note;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const textLength = $computed((): number => {
 | 
			
		||||
| 
						 | 
				
			
			@ -342,7 +342,7 @@ function focus() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function chooseFileFrom(ev) {
 | 
			
		||||
	selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => {
 | 
			
		||||
	selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => {
 | 
			
		||||
		for (const file of files_) {
 | 
			
		||||
			files.push(file);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -371,11 +371,6 @@ function upload(file: File, name?: string) {
 | 
			
		|||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onPollUpdate(poll) {
 | 
			
		||||
	poll = poll;
 | 
			
		||||
	saveDraft();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setVisibility() {
 | 
			
		||||
	if (props.channel) {
 | 
			
		||||
		// TODO: information dialog
 | 
			
		||||
| 
						 | 
				
			
			@ -452,7 +447,7 @@ async function onPaste(e: ClipboardEvent) {
 | 
			
		|||
 | 
			
		||||
		os.confirm({
 | 
			
		||||
			type: 'info',
 | 
			
		||||
			text: i18n.locale.quoteQuestion,
 | 
			
		||||
			text: i18n.ts.quoteQuestion,
 | 
			
		||||
		}).then(({ canceled }) => {
 | 
			
		||||
			if (canceled) {
 | 
			
		||||
				insertTextAtCursor(textareaEl, paste);
 | 
			
		||||
| 
						 | 
				
			
			@ -597,7 +592,7 @@ function insertMention() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
async function insertEmoji(ev: MouseEvent) {
 | 
			
		||||
	os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
 | 
			
		||||
	os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textareaEl);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showActions(ev) {
 | 
			
		||||
| 
						 | 
				
			
			@ -610,7 +605,7 @@ function showActions(ev) {
 | 
			
		|||
				if (key === 'text') { text = value; }
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	})), ev.currentTarget || ev.target);
 | 
			
		||||
	})), ev.currentTarget ?? ev.target);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
 | 
			
		||||
| 
						 | 
				
			
			@ -646,7 +641,7 @@ onMounted(() => {
 | 
			
		|||
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		// 書きかけの投稿を復元
 | 
			
		||||
		if (!props.share && !props.mention && !props.specified) {
 | 
			
		||||
		if (!props.instant && !props.mention && !props.specified) {
 | 
			
		||||
			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
 | 
			
		||||
			if (draft) {
 | 
			
		||||
				text = draft.data.text;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,7 +59,7 @@ export default defineComponent({
 | 
			
		|||
		const renote = (viaKeyboard = false) => {
 | 
			
		||||
			pleaseLogin();
 | 
			
		||||
			os.popupMenu([{
 | 
			
		||||
				text: i18n.locale.renote,
 | 
			
		||||
				text: i18n.ts.renote,
 | 
			
		||||
				icon: 'fas fa-retweet',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					os.api('notes/create', {
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +67,7 @@ export default defineComponent({
 | 
			
		|||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.locale.quote,
 | 
			
		||||
				text: i18n.ts.quote,
 | 
			
		||||
				icon: 'fas fa-quote-right',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					os.post({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -109,7 +109,7 @@ export default defineComponent({
 | 
			
		|||
				text: 'Delete some bananas',
 | 
			
		||||
				danger: true,
 | 
			
		||||
				action: () => {},
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
			}], ev.currentTarget ?? ev.target);
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mk-toast">
 | 
			
		||||
	<transition name="toast" appear @after-leave="emit('closed')">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'toast' : ''" appear @after-leave="emit('closed')">
 | 
			
		||||
		<div v-if="showing" class="body _acrylic" :style="{ zIndex }">
 | 
			
		||||
			<div class="message">
 | 
			
		||||
				{{ message }}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</header>
 | 
			
		||||
	<transition name="container-toggle"
 | 
			
		||||
	<transition :name="$store.state.animation ? 'container-toggle' : ''"
 | 
			
		||||
		@enter="enter"
 | 
			
		||||
		@after-enter="afterEnter"
 | 
			
		||||
		@leave="leave"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
			<template v-else><i class="fas fa-angle-down"></i></template>
 | 
			
		||||
		</button>
 | 
			
		||||
	</header>
 | 
			
		||||
	<transition name="folder-toggle"
 | 
			
		||||
	<transition :name="$store.state.animation ? 'folder-toggle' : ''"
 | 
			
		||||
		@enter="enter"
 | 
			
		||||
		@after-enter="afterEnter"
 | 
			
		||||
		@leave="leave"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="$emit('closed')" @enter="$emit('opening')" @after-enter="childRendered">
 | 
			
		||||
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered">
 | 
			
		||||
	<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
 | 
			
		||||
		<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
 | 
			
		||||
		<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
 | 
			
		||||
| 
						 | 
				
			
			@ -9,8 +9,8 @@
 | 
			
		|||
</transition>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, nextTick, onMounted, computed, PropType, ref, watch } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { nextTick, onMounted, computed, ref, watch, provide } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { isTouchUsing } from '@/scripts/touch';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
| 
						 | 
				
			
			@ -25,234 +25,206 @@ function getFixedContainer(el: Element | null): Element | null {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	provide: {
 | 
			
		||||
		modal: true
 | 
			
		||||
	},
 | 
			
		||||
type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		manualShowing: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: null,
 | 
			
		||||
		},
 | 
			
		||||
		srcCenter: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		src: {
 | 
			
		||||
			type: Object as PropType<HTMLElement>,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: null,
 | 
			
		||||
		},
 | 
			
		||||
		preferType: {
 | 
			
		||||
			required: false,
 | 
			
		||||
			type: String,
 | 
			
		||||
			default: 'auto',
 | 
			
		||||
		},
 | 
			
		||||
		zPriority: {
 | 
			
		||||
			type: String as PropType<'low' | 'middle' | 'high'>,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: 'low',
 | 
			
		||||
		},
 | 
			
		||||
		noOverlap: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: true,
 | 
			
		||||
		},
 | 
			
		||||
		transparentBg: {
 | 
			
		||||
			type: Boolean,
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	manualShowing?: boolean | null;
 | 
			
		||||
	srcCenter?: boolean;
 | 
			
		||||
	src?: HTMLElement;
 | 
			
		||||
	preferType?: ModalTypes | 'auto';
 | 
			
		||||
	zPriority?: 'low' | 'middle' | 'high';
 | 
			
		||||
	noOverlap?: boolean;
 | 
			
		||||
	transparentBg?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	manualShowing: null,
 | 
			
		||||
	src: null,
 | 
			
		||||
	preferType: 'auto',
 | 
			
		||||
	zPriority: 'low',
 | 
			
		||||
	noOverlap: true,
 | 
			
		||||
	transparentBg: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
	emits: ['opening', 'click', 'esc', 'close', 'closed'],
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'opening'): void;
 | 
			
		||||
	(ev: 'click'): void;
 | 
			
		||||
	(ev: 'esc'): void;
 | 
			
		||||
	(ev: 'close'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const maxHeight = ref<number>();
 | 
			
		||||
		const fixed = ref(false);
 | 
			
		||||
		const transformOrigin = ref('center');
 | 
			
		||||
		const showing = ref(true);
 | 
			
		||||
		const content = ref<HTMLElement>();
 | 
			
		||||
		const zIndex = os.claimZIndex(props.zPriority);
 | 
			
		||||
		const type = computed(() => {
 | 
			
		||||
			if (props.preferType === 'auto') {
 | 
			
		||||
				if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
 | 
			
		||||
					return 'drawer';
 | 
			
		||||
				} else {
 | 
			
		||||
					return props.src != null ? 'popup' : 'dialog';
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				return props.preferType;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
		
 | 
			
		||||
		let contentClicking = false;
 | 
			
		||||
provide('modal', true);
 | 
			
		||||
 | 
			
		||||
		const close = () => {
 | 
			
		||||
			// eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
			if (props.src) props.src.style.pointerEvents = 'auto';
 | 
			
		||||
			showing.value = false;
 | 
			
		||||
			context.emit('close');
 | 
			
		||||
		};
 | 
			
		||||
const maxHeight = ref<number>();
 | 
			
		||||
const fixed = ref(false);
 | 
			
		||||
const transformOrigin = ref('center');
 | 
			
		||||
const showing = ref(true);
 | 
			
		||||
const content = ref<HTMLElement>();
 | 
			
		||||
const zIndex = os.claimZIndex(props.zPriority);
 | 
			
		||||
const type = computed(() => {
 | 
			
		||||
	if (props.preferType === 'auto') {
 | 
			
		||||
		if (!defaultStore.state.disableDrawer && isTouchUsing && window.innerWidth < 500 && window.innerHeight < 1000) {
 | 
			
		||||
			return 'drawer';
 | 
			
		||||
		} else {
 | 
			
		||||
			return props.src != null ? 'popup' : 'dialog';
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		return props.preferType!;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		const onBgClick = () => {
 | 
			
		||||
			if (contentClicking) return;
 | 
			
		||||
			context.emit('click');
 | 
			
		||||
		};
 | 
			
		||||
let contentClicking = false;
 | 
			
		||||
 | 
			
		||||
		if (type.value === 'drawer') {
 | 
			
		||||
			maxHeight.value = window.innerHeight / 2;
 | 
			
		||||
const close = () => {
 | 
			
		||||
	// eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
	if (props.src) props.src.style.pointerEvents = 'auto';
 | 
			
		||||
	showing.value = false;
 | 
			
		||||
	emit('close');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onBgClick = () => {
 | 
			
		||||
	if (contentClicking) return;
 | 
			
		||||
	emit('click');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (type.value === 'drawer') {
 | 
			
		||||
	maxHeight.value = window.innerHeight / 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const keymap = {
 | 
			
		||||
	'esc': () => emit('esc'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MARGIN = 16;
 | 
			
		||||
 | 
			
		||||
const align = () => {
 | 
			
		||||
	if (props.src == null) return;
 | 
			
		||||
	if (type.value === 'drawer') return;
 | 
			
		||||
 | 
			
		||||
	const popover = content.value!;
 | 
			
		||||
 | 
			
		||||
	if (popover == null) return;
 | 
			
		||||
 | 
			
		||||
	const rect = props.src.getBoundingClientRect();
 | 
			
		||||
	
 | 
			
		||||
	const width = popover.offsetWidth;
 | 
			
		||||
	const height = popover.offsetHeight;
 | 
			
		||||
 | 
			
		||||
	let left;
 | 
			
		||||
	let top;
 | 
			
		||||
 | 
			
		||||
	if (props.srcCenter) {
 | 
			
		||||
		const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
 | 
			
		||||
		const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
 | 
			
		||||
		left = (x - (width / 2));
 | 
			
		||||
		top = (y - (height / 2));
 | 
			
		||||
	} else {
 | 
			
		||||
		const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
 | 
			
		||||
		const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
 | 
			
		||||
		left = (x - (width / 2));
 | 
			
		||||
		top = y;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (fixed.value) {
 | 
			
		||||
		// 画面から横にはみ出る場合
 | 
			
		||||
		if (left + width > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - width;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const keymap = {
 | 
			
		||||
			'esc': () => context.emit('esc'),
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const MARGIN = 16;
 | 
			
		||||
 | 
			
		||||
		const align = () => {
 | 
			
		||||
			if (props.src == null) return;
 | 
			
		||||
			if (type.value === 'drawer') return;
 | 
			
		||||
 | 
			
		||||
			const popover = content.value!;
 | 
			
		||||
 | 
			
		||||
			if (popover == null) return;
 | 
			
		||||
 | 
			
		||||
			const rect = props.src.getBoundingClientRect();
 | 
			
		||||
			
 | 
			
		||||
			const width = popover.offsetWidth;
 | 
			
		||||
			const height = popover.offsetHeight;
 | 
			
		||||
 | 
			
		||||
			let left;
 | 
			
		||||
			let top;
 | 
			
		||||
 | 
			
		||||
			if (props.srcCenter) {
 | 
			
		||||
				const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
 | 
			
		||||
				const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + (props.src.offsetHeight / 2);
 | 
			
		||||
				left = (x - (width / 2));
 | 
			
		||||
				top = (y - (height / 2));
 | 
			
		||||
			} else {
 | 
			
		||||
				const x = rect.left + (fixed.value ? 0 : window.pageXOffset) + (props.src.offsetWidth / 2);
 | 
			
		||||
				const y = rect.top + (fixed.value ? 0 : window.pageYOffset) + props.src.offsetHeight;
 | 
			
		||||
				left = (x - (width / 2));
 | 
			
		||||
				top = y;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (fixed.value) {
 | 
			
		||||
				// 画面から横にはみ出る場合
 | 
			
		||||
				if (left + width > window.innerWidth) {
 | 
			
		||||
					left = window.innerWidth - width;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 画面から縦にはみ出る場合
 | 
			
		||||
				if (top + height > (window.innerHeight - MARGIN)) {
 | 
			
		||||
					if (props.noOverlap) {
 | 
			
		||||
						const underSpace = (window.innerHeight - MARGIN) - top;
 | 
			
		||||
						const upperSpace = (rect.top - MARGIN);
 | 
			
		||||
						if (underSpace >= (upperSpace / 3)) {
 | 
			
		||||
							maxHeight.value =  underSpace;
 | 
			
		||||
						} else {
 | 
			
		||||
							maxHeight.value =  upperSpace;
 | 
			
		||||
							top = (upperSpace + MARGIN) - height;
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						top = (window.innerHeight - MARGIN) - height;
 | 
			
		||||
					}
 | 
			
		||||
		// 画面から縦にはみ出る場合
 | 
			
		||||
		if (top + height > (window.innerHeight - MARGIN)) {
 | 
			
		||||
			if (props.noOverlap) {
 | 
			
		||||
				const underSpace = (window.innerHeight - MARGIN) - top;
 | 
			
		||||
				const upperSpace = (rect.top - MARGIN);
 | 
			
		||||
				if (underSpace >= (upperSpace / 3)) {
 | 
			
		||||
					maxHeight.value =  underSpace;
 | 
			
		||||
				} else {
 | 
			
		||||
					maxHeight.value =  upperSpace;
 | 
			
		||||
					top = (upperSpace + MARGIN) - height;
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				// 画面から横にはみ出る場合
 | 
			
		||||
				if (left + width - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
					left = window.innerWidth - width + window.pageXOffset - 1;
 | 
			
		||||
				top = (window.innerHeight - MARGIN) - height;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// 画面から横にはみ出る場合
 | 
			
		||||
		if (left + width - window.pageXOffset > window.innerWidth) {
 | 
			
		||||
			left = window.innerWidth - width + window.pageXOffset - 1;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 画面から縦にはみ出る場合
 | 
			
		||||
		if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
 | 
			
		||||
			if (props.noOverlap) {
 | 
			
		||||
				const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
 | 
			
		||||
				const upperSpace = (rect.top - MARGIN);
 | 
			
		||||
				if (underSpace >= (upperSpace / 3)) {
 | 
			
		||||
					maxHeight.value =  underSpace;
 | 
			
		||||
				} else {
 | 
			
		||||
					maxHeight.value =  upperSpace;
 | 
			
		||||
					top = window.pageYOffset + ((upperSpace + MARGIN) - height);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 画面から縦にはみ出る場合
 | 
			
		||||
				if (top + height - window.pageYOffset > (window.innerHeight - MARGIN)) {
 | 
			
		||||
					if (props.noOverlap) {
 | 
			
		||||
						const underSpace = (window.innerHeight - MARGIN) - (top - window.pageYOffset);
 | 
			
		||||
						const upperSpace = (rect.top - MARGIN);
 | 
			
		||||
						if (underSpace >= (upperSpace / 3)) {
 | 
			
		||||
							maxHeight.value =  underSpace;
 | 
			
		||||
						} else {
 | 
			
		||||
							maxHeight.value =  upperSpace;
 | 
			
		||||
							top = window.pageYOffset + ((upperSpace + MARGIN) - height);
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (top < 0) {
 | 
			
		||||
				top = MARGIN;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (left < 0) {
 | 
			
		||||
				left = 0;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
 | 
			
		||||
				transformOrigin.value = 'center top';
 | 
			
		||||
			} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
 | 
			
		||||
				transformOrigin.value = 'center bottom';
 | 
			
		||||
			} else {
 | 
			
		||||
				transformOrigin.value = 'center';
 | 
			
		||||
				top = (window.innerHeight - MARGIN) - height + window.pageYOffset - 1;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
			popover.style.left = left + 'px';
 | 
			
		||||
			popover.style.top = top + 'px';
 | 
			
		||||
		};
 | 
			
		||||
	if (top < 0) {
 | 
			
		||||
		top = MARGIN;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		const childRendered = () => {
 | 
			
		||||
			// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 | 
			
		||||
			const el = content.value!.children[0];
 | 
			
		||||
			el.addEventListener('mousedown', e => {
 | 
			
		||||
				contentClicking = true;
 | 
			
		||||
				window.addEventListener('mouseup', e => {
 | 
			
		||||
					// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
 | 
			
		||||
					window.setTimeout(() => {
 | 
			
		||||
						contentClicking = false;
 | 
			
		||||
					}, 100);
 | 
			
		||||
				}, { passive: true, once: true });
 | 
			
		||||
			}, { passive: true });
 | 
			
		||||
		};
 | 
			
		||||
	if (left < 0) {
 | 
			
		||||
		left = 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		onMounted(() => {
 | 
			
		||||
			watch(() => props.src, async () => {
 | 
			
		||||
				if (props.src) {
 | 
			
		||||
					// eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
					props.src.style.pointerEvents = 'none';
 | 
			
		||||
				}
 | 
			
		||||
				fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
 | 
			
		||||
	if (top > rect.top + (fixed.value ? 0 : window.pageYOffset)) {
 | 
			
		||||
		transformOrigin.value = 'center top';
 | 
			
		||||
	} else if ((top + height) <= rect.top + (fixed.value ? 0 : window.pageYOffset)) {
 | 
			
		||||
		transformOrigin.value = 'center bottom';
 | 
			
		||||
	} else {
 | 
			
		||||
		transformOrigin.value = 'center';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
				await nextTick()
 | 
			
		||||
				
 | 
			
		||||
				align();
 | 
			
		||||
			}, { immediate: true, });
 | 
			
		||||
	popover.style.left = left + 'px';
 | 
			
		||||
	popover.style.top = top + 'px';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				const popover = content.value;
 | 
			
		||||
				new ResizeObserver((entries, observer) => {
 | 
			
		||||
					align();
 | 
			
		||||
				}).observe(popover!);
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
const childRendered = () => {
 | 
			
		||||
	// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
 | 
			
		||||
	const el = content.value!.children[0];
 | 
			
		||||
	el.addEventListener('mousedown', ev => {
 | 
			
		||||
		contentClicking = true;
 | 
			
		||||
		window.addEventListener('mouseup', ev => {
 | 
			
		||||
			// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
 | 
			
		||||
			window.setTimeout(() => {
 | 
			
		||||
				contentClicking = false;
 | 
			
		||||
			}, 100);
 | 
			
		||||
		}, { passive: true, once: true });
 | 
			
		||||
	}, { passive: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			showing,
 | 
			
		||||
			type,
 | 
			
		||||
			fixed,
 | 
			
		||||
			content,
 | 
			
		||||
			transformOrigin,
 | 
			
		||||
			maxHeight,
 | 
			
		||||
			close,
 | 
			
		||||
			zIndex,
 | 
			
		||||
			keymap,
 | 
			
		||||
			onBgClick,
 | 
			
		||||
			childRendered,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	watch(() => props.src, async () => {
 | 
			
		||||
		if (props.src) {
 | 
			
		||||
			// eslint-disable-next-line vue/no-mutating-props
 | 
			
		||||
			props.src.style.pointerEvents = 'none';
 | 
			
		||||
		}
 | 
			
		||||
		fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
 | 
			
		||||
 | 
			
		||||
		await nextTick()
 | 
			
		||||
		
 | 
			
		||||
		align();
 | 
			
		||||
	}, { immediate: true, });
 | 
			
		||||
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		const popover = content.value;
 | 
			
		||||
		new ResizeObserver((entries, observer) => {
 | 
			
		||||
			align();
 | 
			
		||||
		}).observe(popover!);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	close,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
<transition name="fade" mode="out-in">
 | 
			
		||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 | 
			
		||||
	<MkLoading v-if="fetching"/>
 | 
			
		||||
 | 
			
		||||
	<MkError v-else-if="error" @retry="init()"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
<transition name="tooltip" appear @after-leave="$emit('closed')">
 | 
			
		||||
<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')">
 | 
			
		||||
	<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
 | 
			
		||||
		<slot>{{ text }}</slot>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
 | 
			
		||||
	<transition name="zoom" @after-leave="$emit('closed')">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'zoom' : ''" @after-leave="$emit('closed')">
 | 
			
		||||
		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
 | 
			
		||||
	</transition>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
	<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 v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 | 
			
		||||
		<component :is="self ? 'MkA' : 'a'" v-if="!fetching" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
 | 
			
		||||
			<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
 | 
			
		||||
				<button v-if="!playerEnabled && player.url" class="_button" :title="$ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,10 +13,10 @@ const props = defineProps<{
 | 
			
		|||
 | 
			
		||||
const text = $computed(() => {
 | 
			
		||||
	switch (props.user.onlineStatus) {
 | 
			
		||||
		case 'online': return i18n.locale.online;
 | 
			
		||||
		case 'active': return i18n.locale.active;
 | 
			
		||||
		case 'offline': return i18n.locale.offline;
 | 
			
		||||
		case 'unknown': return i18n.locale.unknown;
 | 
			
		||||
		case 'online': return i18n.ts.online;
 | 
			
		||||
		case 'active': return i18n.ts.active;
 | 
			
		||||
		case 'offline': return i18n.ts.offline;
 | 
			
		||||
		case 'unknown': return i18n.ts.unknown;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
<template>
 | 
			
		||||
<transition name="popup" appear @after-leave="$emit('closed')">
 | 
			
		||||
<transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="$emit('closed')">
 | 
			
		||||
	<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
 | 
			
		||||
		<div v-if="fetched" class="info">
 | 
			
		||||
			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										44
									
								
								packages/client/src/const.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/client/src/const.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
// ブラウザで直接表示することを許可するファイルの種類のリスト
 | 
			
		||||
// ここに含まれないものは application/octet-stream としてレスポンスされる
 | 
			
		||||
// SVGはXSSを生むので許可しない
 | 
			
		||||
export const FILE_TYPE_BROWSERSAFE = [
 | 
			
		||||
	// Images
 | 
			
		||||
	'image/png',
 | 
			
		||||
	'image/gif',
 | 
			
		||||
	'image/jpeg',
 | 
			
		||||
	'image/webp',
 | 
			
		||||
	'image/apng',
 | 
			
		||||
	'image/bmp',
 | 
			
		||||
	'image/tiff',
 | 
			
		||||
	'image/x-icon',
 | 
			
		||||
 | 
			
		||||
	// OggS
 | 
			
		||||
	'audio/opus',
 | 
			
		||||
	'video/ogg',
 | 
			
		||||
	'audio/ogg',
 | 
			
		||||
	'application/ogg',
 | 
			
		||||
 | 
			
		||||
	// ISO/IEC base media file format
 | 
			
		||||
	'video/quicktime',
 | 
			
		||||
	'video/mp4',
 | 
			
		||||
	'audio/mp4',
 | 
			
		||||
	'video/x-m4v',
 | 
			
		||||
	'audio/x-m4a',
 | 
			
		||||
	'video/3gpp',
 | 
			
		||||
	'video/3gpp2',
 | 
			
		||||
 | 
			
		||||
	'video/mpeg',
 | 
			
		||||
	'audio/mpeg',
 | 
			
		||||
 | 
			
		||||
	'video/webm',
 | 
			
		||||
	'audio/webm',
 | 
			
		||||
 | 
			
		||||
	'audio/aac',
 | 
			
		||||
	'audio/x-flac',
 | 
			
		||||
	'audio/vnd.wave',
 | 
			
		||||
];
 | 
			
		||||
/*
 | 
			
		||||
https://github.com/sindresorhus/file-type/blob/main/supported.js
 | 
			
		||||
https://github.com/sindresorhus/file-type/blob/main/core.js
 | 
			
		||||
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
 | 
			
		||||
*/
 | 
			
		||||
| 
						 | 
				
			
			@ -188,7 +188,7 @@ app.config.globalProperties = {
 | 
			
		|||
	$store: defaultStore,
 | 
			
		||||
	$instance: instance,
 | 
			
		||||
	$t: i18n.t,
 | 
			
		||||
	$ts: i18n.locale,
 | 
			
		||||
	$ts: i18n.ts,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
app.use(router);
 | 
			
		||||
| 
						 | 
				
			
			@ -305,8 +305,8 @@ stream.on('_disconnected_', async () => {
 | 
			
		|||
		reloadDialogShowing = true;
 | 
			
		||||
		const { canceled } = await confirm({
 | 
			
		||||
			type: 'warning',
 | 
			
		||||
			title: i18n.locale.disconnectedFromServer,
 | 
			
		||||
			text: i18n.locale.reloadConfirm,
 | 
			
		||||
			title: i18n.ts.disconnectedFromServer,
 | 
			
		||||
			text: i18n.ts.reloadConfirm,
 | 
			
		||||
		});
 | 
			
		||||
		reloadDialogShowing = false;
 | 
			
		||||
		if (!canceled) {
 | 
			
		||||
| 
						 | 
				
			
			@ -330,7 +330,7 @@ if ($i) {
 | 
			
		|||
	if ($i.isDeleted) {
 | 
			
		||||
		alert({
 | 
			
		||||
			type: 'warning',
 | 
			
		||||
			text: i18n.locale.accountDeletionInProgress,
 | 
			
		||||
			text: i18n.ts.accountDeletionInProgress,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,12 +73,12 @@ export const menuDef = reactive({
 | 
			
		|||
				})), null, {
 | 
			
		||||
					type: 'link',
 | 
			
		||||
					to: '/my/lists',
 | 
			
		||||
					text: i18n.locale.manageLists,
 | 
			
		||||
					text: i18n.ts.manageLists,
 | 
			
		||||
					icon: 'fas fa-cog',
 | 
			
		||||
				}];
 | 
			
		||||
				items.value = _items;
 | 
			
		||||
			});
 | 
			
		||||
			os.popupMenu(items, ev.currentTarget || ev.target);
 | 
			
		||||
			os.popupMenu(items, ev.currentTarget ?? ev.target);
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	groups: {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,12 +104,12 @@ export const menuDef = reactive({
 | 
			
		|||
				})), null, {
 | 
			
		||||
					type: 'link',
 | 
			
		||||
					to: '/my/antennas',
 | 
			
		||||
					text: i18n.locale.manageAntennas,
 | 
			
		||||
					text: i18n.ts.manageAntennas,
 | 
			
		||||
					icon: 'fas fa-cog',
 | 
			
		||||
				}];
 | 
			
		||||
				items.value = _items;
 | 
			
		||||
			});
 | 
			
		||||
			os.popupMenu(items, ev.currentTarget || ev.target);
 | 
			
		||||
			os.popupMenu(items, ev.currentTarget ?? ev.target);
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	mentions: {
 | 
			
		||||
| 
						 | 
				
			
			@ -173,34 +173,34 @@ export const menuDef = reactive({
 | 
			
		|||
		icon: 'fas fa-columns',
 | 
			
		||||
		action: (ev) => {
 | 
			
		||||
			os.popupMenu([{
 | 
			
		||||
				text: i18n.locale.default,
 | 
			
		||||
				text: i18n.ts.default,
 | 
			
		||||
				active: ui === 'default' || ui === null,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					localStorage.setItem('ui', 'default');
 | 
			
		||||
					unisonReload();
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.locale.deck,
 | 
			
		||||
				text: i18n.ts.deck,
 | 
			
		||||
				active: ui === 'deck',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					localStorage.setItem('ui', 'deck');
 | 
			
		||||
					unisonReload();
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.locale.classic,
 | 
			
		||||
				text: i18n.ts.classic,
 | 
			
		||||
				active: ui === 'classic',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					localStorage.setItem('ui', 'classic');
 | 
			
		||||
					unisonReload();
 | 
			
		||||
				}
 | 
			
		||||
			}, /*{
 | 
			
		||||
				text: i18n.locale.desktop + ' (β)',
 | 
			
		||||
				text: i18n.ts.desktop + ' (β)',
 | 
			
		||||
				active: ui === 'desktop',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					localStorage.setItem('ui', 'desktop');
 | 
			
		||||
					unisonReload();
 | 
			
		||||
				}
 | 
			
		||||
			}*/], ev.currentTarget || ev.target);
 | 
			
		||||
			}*/], ev.currentTarget ?? ev.target);
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -403,7 +403,7 @@ export async function selectDriveFolder(multiple: boolean) {
 | 
			
		|||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function pickEmoji(src?: HTMLElement, opts) {
 | 
			
		||||
export async function pickEmoji(src: HTMLElement | null, opts) {
 | 
			
		||||
	return new Promise((resolve, reject) => {
 | 
			
		||||
		popup(import('@/components/emoji-picker-dialog.vue'), {
 | 
			
		||||
			src,
 | 
			
		||||
| 
						 | 
				
			
			@ -570,7 +570,7 @@ export function upload(file: File, folder?: any, name?: string): Promise<Misskey
 | 
			
		|||
			const xhr = new XMLHttpRequest();
 | 
			
		||||
			xhr.open('POST', apiUrl + '/drive/files/create', true);
 | 
			
		||||
			xhr.onload = (ev) => {
 | 
			
		||||
				if (ev.target == null || ev.target.response == null) {
 | 
			
		||||
				if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
 | 
			
		||||
					// TODO: 消すのではなくて再送できるようにしたい
 | 
			
		||||
					uploads.value = uploads.value.filter(x => x.id != id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,15 +3,15 @@
 | 
			
		|||
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
 | 
			
		||||
	<div v-show="loaded" class="mjndxjch">
 | 
			
		||||
		<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
 | 
			
		||||
		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p>
 | 
			
		||||
		<p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p>
 | 
			
		||||
		<p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p>
 | 
			
		||||
		<p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.ts.pageLoadError }}</b></p>
 | 
			
		||||
		<p v-if="meta && (version === meta.version)">{{ i18n.ts.pageLoadErrorDescription }}</p>
 | 
			
		||||
		<p v-else-if="serverIsDead">{{ i18n.ts.serverIsDead }}</p>
 | 
			
		||||
		<template v-else>
 | 
			
		||||
			<p>{{ i18n.locale.newVersionOfClientAvailable }}</p>
 | 
			
		||||
			<p>{{ i18n.locale.youShouldUpgradeClient }}</p>
 | 
			
		||||
			<MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton>
 | 
			
		||||
			<p>{{ i18n.ts.newVersionOfClientAvailable }}</p>
 | 
			
		||||
			<p>{{ i18n.ts.youShouldUpgradeClient }}</p>
 | 
			
		||||
			<MkButton class="button primary" @click="reload">{{ i18n.ts.reload }}</MkButton>
 | 
			
		||||
		</template>
 | 
			
		||||
		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p>
 | 
			
		||||
		<p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></p>
 | 
			
		||||
		<p v-if="error" class="error">ERROR: {{ error }}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
</transition>
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +54,7 @@ function reload() {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.error,
 | 
			
		||||
		title: i18n.ts.error,
 | 
			
		||||
		icon: 'fas fa-exclamation-triangle',
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@
 | 
			
		|||
				<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_formBlock" style="text-align: center;">
 | 
			
		||||
				{{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a>
 | 
			
		||||
				{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="_formBlock" style="text-align: center;">
 | 
			
		||||
				<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -19,23 +19,23 @@
 | 
			
		|||
				<div class="_formLinks">
 | 
			
		||||
					<FormLink to="https://github.com/misskey-dev/misskey" external>
 | 
			
		||||
						<template #icon><i class="fas fa-code"></i></template>
 | 
			
		||||
						{{ i18n.locale._aboutMisskey.source }}
 | 
			
		||||
						{{ i18n.ts._aboutMisskey.source }}
 | 
			
		||||
						<template #suffix>GitHub</template>
 | 
			
		||||
					</FormLink>
 | 
			
		||||
					<FormLink to="https://crowdin.com/project/misskey" external>
 | 
			
		||||
						<template #icon><i class="fas fa-language"></i></template>
 | 
			
		||||
						{{ i18n.locale._aboutMisskey.translation }}
 | 
			
		||||
						{{ i18n.ts._aboutMisskey.translation }}
 | 
			
		||||
						<template #suffix>Crowdin</template>
 | 
			
		||||
					</FormLink>
 | 
			
		||||
					<FormLink to="https://www.patreon.com/syuilo" external>
 | 
			
		||||
						<template #icon><i class="fas fa-hand-holding-medical"></i></template>
 | 
			
		||||
						{{ i18n.locale._aboutMisskey.donate }}
 | 
			
		||||
						{{ i18n.ts._aboutMisskey.donate }}
 | 
			
		||||
						<template #suffix>Patreon</template>
 | 
			
		||||
					</FormLink>
 | 
			
		||||
				</div>
 | 
			
		||||
			</FormSection>
 | 
			
		||||
			<FormSection>
 | 
			
		||||
				<template #label>{{ i18n.locale._aboutMisskey.contributors }}</template>
 | 
			
		||||
				<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
 | 
			
		||||
				<div class="_formLinks">
 | 
			
		||||
					<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
 | 
			
		||||
					<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
 | 
			
		||||
| 
						 | 
				
			
			@ -47,12 +47,12 @@
 | 
			
		|||
					<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
 | 
			
		||||
					<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
 | 
			
		||||
				</div>
 | 
			
		||||
				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template>
 | 
			
		||||
				<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
 | 
			
		||||
			</FormSection>
 | 
			
		||||
			<FormSection>
 | 
			
		||||
				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template>
 | 
			
		||||
				<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
 | 
			
		||||
				<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
 | 
			
		||||
				<template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.ts._aboutMisskey.morePatrons }}</template>
 | 
			
		||||
			</FormSection>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
| 
						 | 
				
			
			@ -182,6 +182,7 @@ function gravity() {
 | 
			
		|||
function iLoveMisskey() {
 | 
			
		||||
	os.post({
 | 
			
		||||
		initialText: 'I $[jelly ❤] #Misskey',
 | 
			
		||||
		instant: true,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -193,7 +194,7 @@ onBeforeUnmount(() => {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.aboutMisskey,
 | 
			
		||||
		title: i18n.ts.aboutMisskey,
 | 
			
		||||
		icon: null,
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,7 @@ const initStats = () => os.api('stats', {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.instanceInfo,
 | 
			
		||||
		title: i18n.ts.instanceInfo,
 | 
			
		||||
		icon: 'fas fa-info-circle',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -118,7 +118,7 @@ const toggleSelect = (emoji) => {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const add = async (ev: MouseEvent) => {
 | 
			
		||||
	const files = await selectFiles(ev.currentTarget || ev.target, null);
 | 
			
		||||
	const files = await selectFiles(ev.currentTarget ?? ev.target, null);
 | 
			
		||||
 | 
			
		||||
	const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
 | 
			
		||||
		fileId: file.id,
 | 
			
		||||
| 
						 | 
				
			
			@ -157,23 +157,23 @@ const remoteMenu = (emoji, ev: MouseEvent) => {
 | 
			
		|||
		type: 'label',
 | 
			
		||||
		text: ':' + emoji.name + ':',
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.locale.import,
 | 
			
		||||
		text: i18n.ts.import,
 | 
			
		||||
		icon: 'fas fa-plus',
 | 
			
		||||
		action: () => { im(emoji) }
 | 
			
		||||
	}], ev.currentTarget || ev.target);
 | 
			
		||||
	}], ev.currentTarget ?? ev.target);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const menu = (ev: MouseEvent) => {
 | 
			
		||||
	os.popupMenu([{
 | 
			
		||||
		icon: 'fas fa-download',
 | 
			
		||||
		text: i18n.locale.export,
 | 
			
		||||
		text: i18n.ts.export,
 | 
			
		||||
		action: async () => {
 | 
			
		||||
			os.api('export-custom-emojis', {
 | 
			
		||||
			})
 | 
			
		||||
			.then(() => {
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'info',
 | 
			
		||||
					text: i18n.locale.exportRequested,
 | 
			
		||||
					text: i18n.ts.exportRequested,
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e) => {
 | 
			
		||||
				os.alert({
 | 
			
		||||
| 
						 | 
				
			
			@ -184,16 +184,16 @@ const menu = (ev: MouseEvent) => {
 | 
			
		|||
		}
 | 
			
		||||
	}, {
 | 
			
		||||
		icon: 'fas fa-upload',
 | 
			
		||||
		text: i18n.locale.import,
 | 
			
		||||
		text: i18n.ts.import,
 | 
			
		||||
		action: async () => {
 | 
			
		||||
			const file = await selectFile(ev.currentTarget || ev.target);
 | 
			
		||||
			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,
 | 
			
		||||
					text: i18n.ts.importRequested,
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e) => {
 | 
			
		||||
				os.alert({
 | 
			
		||||
| 
						 | 
				
			
			@ -202,7 +202,7 @@ const menu = (ev: MouseEvent) => {
 | 
			
		|||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}], ev.currentTarget || ev.target);
 | 
			
		||||
	}], ev.currentTarget ?? ev.target);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setCategoryBulk = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -256,7 +256,7 @@ const setTagBulk = async () => {
 | 
			
		|||
const delBulk = async () => {
 | 
			
		||||
	const { canceled } = await os.confirm({
 | 
			
		||||
		type: 'warning',
 | 
			
		||||
		text: i18n.locale.deleteConfirm,
 | 
			
		||||
		text: i18n.ts.deleteConfirm,
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
	await os.apiWithDialog('admin/emoji/delete-bulk', {
 | 
			
		||||
| 
						 | 
				
			
			@ -267,13 +267,13 @@ const delBulk = async () => {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: computed(() => ({
 | 
			
		||||
		title: i18n.locale.customEmojis,
 | 
			
		||||
		title: i18n.ts.customEmojis,
 | 
			
		||||
		icon: 'fas fa-laugh',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
		actions: [{
 | 
			
		||||
			asFullButton: true,
 | 
			
		||||
			icon: 'fas fa-plus',
 | 
			
		||||
			text: i18n.locale.addEmoji,
 | 
			
		||||
			text: i18n.ts.addEmoji,
 | 
			
		||||
			handler: add,
 | 
			
		||||
		}, {
 | 
			
		||||
			icon: 'fas fa-ellipsis-h',
 | 
			
		||||
| 
						 | 
				
			
			@ -281,11 +281,11 @@ defineExpose({
 | 
			
		|||
		}],
 | 
			
		||||
		tabs: [{
 | 
			
		||||
			active: tab.value === 'local',
 | 
			
		||||
			title: i18n.locale.local,
 | 
			
		||||
			title: i18n.ts.local,
 | 
			
		||||
			onClick: () => { tab.value = 'local'; },
 | 
			
		||||
		}, {
 | 
			
		||||
			active: tab.value === 'remote',
 | 
			
		||||
			title: i18n.locale.remote,
 | 
			
		||||
			title: i18n.ts.remote,
 | 
			
		||||
			onClick: () => { tab.value = 'remote'; },
 | 
			
		||||
		},]
 | 
			
		||||
	})),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,7 @@ export default defineComponent({
 | 
			
		|||
 | 
			
		||||
	setup(props, context) {
 | 
			
		||||
		const indexInfo = {
 | 
			
		||||
			title: i18n.locale.controlPanel,
 | 
			
		||||
			title: i18n.ts.controlPanel,
 | 
			
		||||
			icon: 'fas fa-cog',
 | 
			
		||||
			bg: 'var(--bg)',
 | 
			
		||||
			hideHeader: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -91,119 +91,119 @@ export default defineComponent({
 | 
			
		|||
		});
 | 
			
		||||
 | 
			
		||||
		const menuDef = computed(() => [{
 | 
			
		||||
			title: i18n.locale.quickAction,
 | 
			
		||||
			title: i18n.ts.quickAction,
 | 
			
		||||
			items: [{
 | 
			
		||||
				type: 'button',
 | 
			
		||||
				icon: 'fas fa-search',
 | 
			
		||||
				text: i18n.locale.lookup,
 | 
			
		||||
				text: i18n.ts.lookup,
 | 
			
		||||
				action: lookup,
 | 
			
		||||
			}, ...(instance.disableRegistration ? [{
 | 
			
		||||
				type: 'button',
 | 
			
		||||
				icon: 'fas fa-user',
 | 
			
		||||
				text: i18n.locale.invite,
 | 
			
		||||
				text: i18n.ts.invite,
 | 
			
		||||
				action: invite,
 | 
			
		||||
			}] : [])],
 | 
			
		||||
		}, {
 | 
			
		||||
			title: i18n.locale.administration,
 | 
			
		||||
			title: i18n.ts.administration,
 | 
			
		||||
			items: [{
 | 
			
		||||
				icon: 'fas fa-tachometer-alt',
 | 
			
		||||
				text: i18n.locale.dashboard,
 | 
			
		||||
				text: i18n.ts.dashboard,
 | 
			
		||||
				to: '/admin/overview',
 | 
			
		||||
				active: page.value === 'overview',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-users',
 | 
			
		||||
				text: i18n.locale.users,
 | 
			
		||||
				text: i18n.ts.users,
 | 
			
		||||
				to: '/admin/users',
 | 
			
		||||
				active: page.value === 'users',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-laugh',
 | 
			
		||||
				text: i18n.locale.customEmojis,
 | 
			
		||||
				text: i18n.ts.customEmojis,
 | 
			
		||||
				to: '/admin/emojis',
 | 
			
		||||
				active: page.value === 'emojis',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-globe',
 | 
			
		||||
				text: i18n.locale.federation,
 | 
			
		||||
				text: i18n.ts.federation,
 | 
			
		||||
				to: '/admin/federation',
 | 
			
		||||
				active: page.value === 'federation',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-clipboard-list',
 | 
			
		||||
				text: i18n.locale.jobQueue,
 | 
			
		||||
				text: i18n.ts.jobQueue,
 | 
			
		||||
				to: '/admin/queue',
 | 
			
		||||
				active: page.value === 'queue',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-cloud',
 | 
			
		||||
				text: i18n.locale.files,
 | 
			
		||||
				text: i18n.ts.files,
 | 
			
		||||
				to: '/admin/files',
 | 
			
		||||
				active: page.value === 'files',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-broadcast-tower',
 | 
			
		||||
				text: i18n.locale.announcements,
 | 
			
		||||
				text: i18n.ts.announcements,
 | 
			
		||||
				to: '/admin/announcements',
 | 
			
		||||
				active: page.value === 'announcements',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-audio-description',
 | 
			
		||||
				text: i18n.locale.ads,
 | 
			
		||||
				text: i18n.ts.ads,
 | 
			
		||||
				to: '/admin/ads',
 | 
			
		||||
				active: page.value === 'ads',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-exclamation-circle',
 | 
			
		||||
				text: i18n.locale.abuseReports,
 | 
			
		||||
				text: i18n.ts.abuseReports,
 | 
			
		||||
				to: '/admin/abuses',
 | 
			
		||||
				active: page.value === 'abuses',
 | 
			
		||||
			}],
 | 
			
		||||
		}, {
 | 
			
		||||
			title: i18n.locale.settings,
 | 
			
		||||
			title: i18n.ts.settings,
 | 
			
		||||
			items: [{
 | 
			
		||||
				icon: 'fas fa-cog',
 | 
			
		||||
				text: i18n.locale.general,
 | 
			
		||||
				text: i18n.ts.general,
 | 
			
		||||
				to: '/admin/settings',
 | 
			
		||||
				active: page.value === 'settings',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-envelope',
 | 
			
		||||
				text: i18n.locale.emailServer,
 | 
			
		||||
				text: i18n.ts.emailServer,
 | 
			
		||||
				to: '/admin/email-settings',
 | 
			
		||||
				active: page.value === 'email-settings',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-cloud',
 | 
			
		||||
				text: i18n.locale.objectStorage,
 | 
			
		||||
				text: i18n.ts.objectStorage,
 | 
			
		||||
				to: '/admin/object-storage',
 | 
			
		||||
				active: page.value === 'object-storage',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-lock',
 | 
			
		||||
				text: i18n.locale.security,
 | 
			
		||||
				text: i18n.ts.security,
 | 
			
		||||
				to: '/admin/security',
 | 
			
		||||
				active: page.value === 'security',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-globe',
 | 
			
		||||
				text: i18n.locale.relays,
 | 
			
		||||
				text: i18n.ts.relays,
 | 
			
		||||
				to: '/admin/relays',
 | 
			
		||||
				active: page.value === 'relays',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-share-alt',
 | 
			
		||||
				text: i18n.locale.integration,
 | 
			
		||||
				text: i18n.ts.integration,
 | 
			
		||||
				to: '/admin/integrations',
 | 
			
		||||
				active: page.value === 'integrations',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-ban',
 | 
			
		||||
				text: i18n.locale.instanceBlocking,
 | 
			
		||||
				text: i18n.ts.instanceBlocking,
 | 
			
		||||
				to: '/admin/instance-block',
 | 
			
		||||
				active: page.value === 'instance-block',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-ghost',
 | 
			
		||||
				text: i18n.locale.proxyAccount,
 | 
			
		||||
				text: i18n.ts.proxyAccount,
 | 
			
		||||
				to: '/admin/proxy-account',
 | 
			
		||||
				active: page.value === 'proxy-account',
 | 
			
		||||
			}, {
 | 
			
		||||
				icon: 'fas fa-cogs',
 | 
			
		||||
				text: i18n.locale.other,
 | 
			
		||||
				text: i18n.ts.other,
 | 
			
		||||
				to: '/admin/other-settings',
 | 
			
		||||
				active: page.value === 'other-settings',
 | 
			
		||||
			}],
 | 
			
		||||
		}, {
 | 
			
		||||
			title: i18n.locale.info,
 | 
			
		||||
			title: i18n.ts.info,
 | 
			
		||||
			items: [{
 | 
			
		||||
				icon: 'fas fa-database',
 | 
			
		||||
				text: i18n.locale.database,
 | 
			
		||||
				text: i18n.ts.database,
 | 
			
		||||
				to: '/admin/database',
 | 
			
		||||
				active: page.value === 'database',
 | 
			
		||||
			}],
 | 
			
		||||
| 
						 | 
				
			
			@ -275,37 +275,37 @@ export default defineComponent({
 | 
			
		|||
 | 
			
		||||
		const lookup = (ev) => {
 | 
			
		||||
			os.popupMenu([{
 | 
			
		||||
				text: i18n.locale.user,
 | 
			
		||||
				text: i18n.ts.user,
 | 
			
		||||
				icon: 'fas fa-user',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					lookupUser();
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.locale.note,
 | 
			
		||||
				text: i18n.ts.note,
 | 
			
		||||
				icon: 'fas fa-pencil-alt',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					alert('TODO');
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.locale.file,
 | 
			
		||||
				text: i18n.ts.file,
 | 
			
		||||
				icon: 'fas fa-cloud',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					alert('TODO');
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.locale.instance,
 | 
			
		||||
				text: i18n.ts.instance,
 | 
			
		||||
				icon: 'fas fa-globe',
 | 
			
		||||
				action: () => {
 | 
			
		||||
					alert('TODO');
 | 
			
		||||
				}
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
			}], ev.currentTarget ?? ev.target);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			[symbols.PAGE_INFO]: INFO,
 | 
			
		||||
			menuDef,
 | 
			
		||||
			header: {
 | 
			
		||||
				title: i18n.locale.controlPanel,
 | 
			
		||||
				title: i18n.ts.controlPanel,
 | 
			
		||||
			},
 | 
			
		||||
			noMaintainerInformation,
 | 
			
		||||
			noBotProtection,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -112,7 +112,7 @@ export default defineComponent({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		setBannerImage(e) {
 | 
			
		||||
			selectFile(e.currentTarget || e.target, null).then(file => {
 | 
			
		||||
			selectFile(e.currentTarget ?? e.target, null).then(file => {
 | 
			
		||||
				this.bannerId = file.id;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,7 +127,7 @@ export default defineComponent({
 | 
			
		|||
						clipId: this.clip.id,
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			} : undefined], ev.currentTarget || ev.target);
 | 
			
		||||
			} : undefined], ev.currentTarget ?? ev.target);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,7 @@ let folder = $ref(null);
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: computed(() => ({
 | 
			
		||||
		title: folder ? folder.name : i18n.locale.drive,
 | 
			
		||||
		title: folder ? folder.name : i18n.ts.drive,
 | 
			
		||||
		icon: 'fas fa-cloud',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
		hideHeader: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,13 +23,13 @@ function menu(ev) {
 | 
			
		|||
		type: 'label',
 | 
			
		||||
		text: ':' + props.emoji.name + ':',
 | 
			
		||||
	}, {
 | 
			
		||||
		text: i18n.locale.copy,
 | 
			
		||||
		text: i18n.ts.copy,
 | 
			
		||||
		icon: 'fas fa-copy',
 | 
			
		||||
		action: () => {
 | 
			
		||||
			copyToClipboard(`:${props.emoji.name}:`);
 | 
			
		||||
			os.success();
 | 
			
		||||
		}
 | 
			
		||||
	}], ev.currentTarget || ev.target);
 | 
			
		||||
	}], ev.currentTarget ?? ev.target);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,14 +16,14 @@ const tab = ref('category');
 | 
			
		|||
function menu(ev) {
 | 
			
		||||
	os.popupMenu([{
 | 
			
		||||
		icon: 'fas fa-download',
 | 
			
		||||
		text: i18n.locale.export,
 | 
			
		||||
		text: i18n.ts.export,
 | 
			
		||||
		action: async () => {
 | 
			
		||||
			os.api('export-custom-emojis', {
 | 
			
		||||
			})
 | 
			
		||||
			.then(() => {
 | 
			
		||||
				os.alert({
 | 
			
		||||
					type: 'info',
 | 
			
		||||
					text: i18n.locale.exportRequested,
 | 
			
		||||
					text: i18n.ts.exportRequested,
 | 
			
		||||
				});
 | 
			
		||||
			}).catch((e) => {
 | 
			
		||||
				os.alert({
 | 
			
		||||
| 
						 | 
				
			
			@ -32,12 +32,12 @@ function menu(ev) {
 | 
			
		|||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}], ev.currentTarget || ev.target);
 | 
			
		||||
	}], ev.currentTarget ?? ev.target);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.customEmojis,
 | 
			
		||||
		title: i18n.ts.customEmojis,
 | 
			
		||||
		icon: 'fas fa-laugh',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
		actions: [{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.favorites,
 | 
			
		||||
		title: i18n.ts.favorites,
 | 
			
		||||
		icon: 'fas fa-star',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ const pagination = {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.featured,
 | 
			
		||||
		title: i18n.ts.featured,
 | 
			
		||||
		icon: 'fas fa-fire-alt',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,7 +135,7 @@ function getStatus(instance) {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.federation,
 | 
			
		||||
		title: i18n.ts.federation,
 | 
			
		||||
		icon: 'fas fa-globe',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ function reject(user) {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: computed(() => ({
 | 
			
		||||
		title: i18n.locale.followRequests,
 | 
			
		||||
		title: i18n.ts.followRequests,
 | 
			
		||||
		icon: 'fas fa-user-clock',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	})),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -92,7 +92,7 @@ export default defineComponent({
 | 
			
		|||
 | 
			
		||||
	methods: {
 | 
			
		||||
		selectFile(e) {
 | 
			
		||||
			selectFiles(e.currentTarget || e.target, null).then(files => {
 | 
			
		||||
			selectFiles(e.currentTarget ?? e.target, null).then(files => {
 | 
			
		||||
				this.files = this.files.concat(files);
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="_root">
 | 
			
		||||
	<transition name="fade" mode="out-in">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
 | 
			
		||||
		<div v-if="post" class="rkxwuolj">
 | 
			
		||||
			<div class="files">
 | 
			
		||||
				<div v-for="file in post.files" :key="file.id" class="file">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,7 @@ const pagination = {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.mentions,
 | 
			
		||||
		title: i18n.ts.mentions,
 | 
			
		||||
		icon: 'fas fa-at',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,14 +12,14 @@ import { i18n } from '@/i18n';
 | 
			
		|||
const pagination = {
 | 
			
		||||
	endpoint: 'notes/mentions' as const,
 | 
			
		||||
	limit: 10,
 | 
			
		||||
	params: () => ({
 | 
			
		||||
	params: {
 | 
			
		||||
		visibility: 'specified'
 | 
			
		||||
	}),
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.directNotes,
 | 
			
		||||
		title: i18n.ts.directNotes,
 | 
			
		||||
		icon: 'fas fa-envelope',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -128,7 +128,7 @@ export default defineComponent({
 | 
			
		|||
				text: this.$ts.messagingWithGroup,
 | 
			
		||||
				icon: 'fas fa-users',
 | 
			
		||||
				action: () => { this.startGroup() }
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
			}], ev.currentTarget ?? ev.target);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		async startUser() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
		ref="text"
 | 
			
		||||
		v-model="text"
 | 
			
		||||
		:placeholder="$ts.inputMessageHere"
 | 
			
		||||
		@keypress="onKeypress"
 | 
			
		||||
		@keydown="onKeydown"
 | 
			
		||||
		@compositionupdate="onCompositionUpdate"
 | 
			
		||||
		@paste="onPaste"
 | 
			
		||||
	></textarea>
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,7 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
import { defineComponent, defineAsyncComponent } from 'vue';
 | 
			
		||||
import insertTextAtCursor from 'insert-text-at-cursor';
 | 
			
		||||
import * as autosize from 'autosize';
 | 
			
		||||
import autosize from 'autosize';
 | 
			
		||||
import { formatTimeString } from '@/scripts/format-time-string';
 | 
			
		||||
import { selectFile } from '@/scripts/select-file';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +76,8 @@ export default defineComponent({
 | 
			
		|||
		autosize(this.$refs.text);
 | 
			
		||||
 | 
			
		||||
		// TODO: detach when unmount
 | 
			
		||||
		new Autocomplete(this.$refs.text, this, { model: 'text' });
 | 
			
		||||
		// TODO
 | 
			
		||||
		//new Autocomplete(this.$refs.text, this, { model: 'text' });
 | 
			
		||||
 | 
			
		||||
		// 書きかけの投稿を復元
 | 
			
		||||
		const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
 | 
			
		||||
| 
						 | 
				
			
			@ -141,7 +142,7 @@ export default defineComponent({
 | 
			
		|||
			//#endregion
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		onKeypress(e) {
 | 
			
		||||
		onKeydown(e) {
 | 
			
		||||
			this.typing();
 | 
			
		||||
			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
 | 
			
		||||
				this.send();
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +154,7 @@ export default defineComponent({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		chooseFile(e) {
 | 
			
		||||
			selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => {
 | 
			
		||||
			selectFile(e.currentTarget ?? e.target, this.$ts.selectFile).then(file => {
 | 
			
		||||
				this.file = file;
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +214,7 @@ export default defineComponent({
 | 
			
		|||
		},
 | 
			
		||||
 | 
			
		||||
		async insertEmoji(ev) {
 | 
			
		||||
			os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
 | 
			
		||||
			os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, this.$refs.text);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
			<button v-show="existMoreMessages" ref="loadMore" class="more _button" :class="{ fetching: fetchingMoreMessages }" :disabled="fetchingMoreMessages" @click="fetchMoreMessages">
 | 
			
		||||
				<template v-if="fetchingMoreMessages"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ fetchingMoreMessages ? $ts.loading : $ts.loadMore }}
 | 
			
		||||
			</button>
 | 
			
		||||
			<XList v-slot="{ item: message }" class="messages" :items="messages" direction="up" reversed>
 | 
			
		||||
			<XList v-if="messages.length > 0" v-slot="{ item: message }" class="messages" :items="messages" direction="up" reversed>
 | 
			
		||||
				<XMessage :key="message.id" :message="message" :is-group="group != null"/>
 | 
			
		||||
			</XList>
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,7 @@
 | 
			
		|||
				</I18n>
 | 
			
		||||
				<MkEllipsis/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<transition name="fade">
 | 
			
		||||
			<transition :name="$store.state.animation ? 'fade' : ''">
 | 
			
		||||
				<div v-show="showIndicator" class="new-message">
 | 
			
		||||
					<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button>
 | 
			
		||||
				</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -335,7 +335,7 @@ const Component = defineComponent({
 | 
			
		|||
					popout(path);
 | 
			
		||||
					this.$router.back();
 | 
			
		||||
				},
 | 
			
		||||
			}], ev.currentTarget || ev.target);
 | 
			
		||||
			}], ev.currentTarget ?? ev.target);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ function onAntennaCreated() {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.manageAntennas,
 | 
			
		||||
		title: i18n.ts.manageAntennas,
 | 
			
		||||
		icon: 'fas fa-satellite',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ import MkPagination from '@/components/ui/pagination.vue';
 | 
			
		|||
import MkButton from '@/components/ui/button.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import * as symbols from '@/symbols';
 | 
			
		||||
import i18n from '@/components/global/i18n';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const pagination = {
 | 
			
		||||
	endpoint: 'clips/list' as const,
 | 
			
		||||
| 
						 | 
				
			
			@ -29,20 +29,20 @@ const pagination = {
 | 
			
		|||
const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
 | 
			
		||||
 | 
			
		||||
async function create() {
 | 
			
		||||
	const { canceled, result } = await os.form(i18n.locale.createNewClip, {
 | 
			
		||||
	const { canceled, result } = await os.form(i18n.ts.createNewClip, {
 | 
			
		||||
		name: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			label: i18n.locale.name,
 | 
			
		||||
			label: i18n.ts.name,
 | 
			
		||||
		},
 | 
			
		||||
		description: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			required: false,
 | 
			
		||||
			multiline: true,
 | 
			
		||||
			label: i18n.locale.description,
 | 
			
		||||
			label: i18n.ts.description,
 | 
			
		||||
		},
 | 
			
		||||
		isPublic: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			label: i18n.locale.public,
 | 
			
		||||
			label: i18n.ts.public,
 | 
			
		||||
			default: false,
 | 
			
		||||
		},
 | 
			
		||||
	});
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +63,7 @@ function onClipDeleted() {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.clip,
 | 
			
		||||
		title: i18n.ts.clip,
 | 
			
		||||
		icon: 'fas fa-paperclip',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
		action: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div class="mk-group-page">
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 | 
			
		||||
		<div v-if="group" class="_section">
 | 
			
		||||
			<div class="_content" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
 | 
			
		||||
				<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
 | 
			
		||||
	<transition name="zoom" mode="out-in">
 | 
			
		||||
	<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 | 
			
		||||
		<div v-if="group" class="_section members _gap">
 | 
			
		||||
			<div class="_title">{{ $ts.members }}</div>
 | 
			
		||||
			<div class="_content">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ const pagination = {
 | 
			
		|||
 | 
			
		||||
async function create() {
 | 
			
		||||
	const { canceled, result: name } = await os.inputText({
 | 
			
		||||
		title: i18n.locale.enterListName,
 | 
			
		||||
		title: i18n.ts.enterListName,
 | 
			
		||||
	});
 | 
			
		||||
	if (canceled) return;
 | 
			
		||||
	await os.apiWithDialog('users/lists/create', { name: name });
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +40,7 @@ async function create() {
 | 
			
		|||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	[symbols.PAGE_INFO]: {
 | 
			
		||||
		title: i18n.locale.manageLists,
 | 
			
		||||
		title: i18n.ts.manageLists,
 | 
			
		||||
		icon: 'fas fa-list-ul',
 | 
			
		||||
		bg: 'var(--bg)',
 | 
			
		||||
		action: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
<template>
 | 
			
		||||
<MkSpacer :content-max="700">
 | 
			
		||||
	<div class="mk-list-page">
 | 
			
		||||
		<transition name="zoom" mode="out-in">
 | 
			
		||||
		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 | 
			
		||||
			<div v-if="list" class="_section">
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
					<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
			</div>
 | 
			
		||||
		</transition>
 | 
			
		||||
 | 
			
		||||
		<transition name="zoom" mode="out-in">
 | 
			
		||||
		<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
 | 
			
		||||
			<div v-if="list" class="_section members _gap">
 | 
			
		||||
				<div class="_title">{{ $ts.members }}</div>
 | 
			
		||||
				<div class="_content">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
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