Merge branch 'develop' into fetch
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								packages/backend/assets/emoji-unknown.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/backend/assets/emoji-unknown.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										29
									
								
								packages/backend/migration/1672822262496-Flash.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								packages/backend/migration/1672822262496-Flash.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
export class Flash1672822262496 {
 | 
			
		||||
    name = 'Flash1672822262496'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "flash" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "title" character varying(256) NOT NULL, "summary" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "script" character varying(16384) NOT NULL, "permissions" character varying(256) array NOT NULL DEFAULT '{}', "likedCount" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_0c01a2c1c5f2266942dd1b3fdbc" PRIMARY KEY ("id")); COMMENT ON COLUMN "flash"."createdAt" IS 'The created date of the Flash.'; COMMENT ON COLUMN "flash"."updatedAt" IS 'The updated date of the Flash.'; COMMENT ON COLUMN "flash"."userId" IS 'The ID of author.'`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_149d2e44785707548c82999b01" ON "flash" ("createdAt") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_3aa8ea9a8f15214ad91638c0a7" ON "flash" ("updatedAt") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_9b88250fc2fd009b8f1b5623ed" ON "flash" ("userId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "flash_like" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "flashId" character varying(32) NOT NULL, CONSTRAINT "PK_d110109ee310588d63d6183b233" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_60c4af1c19a7a75f1592f93b28" ON "flash_like" ("userId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_cfbfeeccb0cbedcd660b17eb07" ON "flash_like" ("userId", "flashId") `);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "flash" ADD CONSTRAINT "FK_9b88250fc2fd009b8f1b5623ed5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "flash_like" ADD CONSTRAINT "FK_60c4af1c19a7a75f1592f93b287" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "flash_like" ADD CONSTRAINT "FK_6c16fe0e93b7a1951eca624b76a" FOREIGN KEY ("flashId") REFERENCES "flash"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "flash_like" DROP CONSTRAINT "FK_6c16fe0e93b7a1951eca624b76a"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "flash_like" DROP CONSTRAINT "FK_60c4af1c19a7a75f1592f93b287"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "flash" DROP CONSTRAINT "FK_9b88250fc2fd009b8f1b5623ed5"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_cfbfeeccb0cbedcd660b17eb07"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_60c4af1c19a7a75f1592f93b28"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "flash_like"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_9b88250fc2fd009b8f1b5623ed"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_3aa8ea9a8f15214ad91638c0a7"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_149d2e44785707548c82999b01"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "flash"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,9 +21,9 @@
 | 
			
		||||
		"@tensorflow/tfjs-node": "4.1.0"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@bull-board/api": "^4.10.0",
 | 
			
		||||
		"@bull-board/fastify": "^4.10.0",
 | 
			
		||||
		"@bull-board/ui": "^4.10.0",
 | 
			
		||||
		"@bull-board/api": "^4.10.1",
 | 
			
		||||
		"@bull-board/fastify": "^4.10.1",
 | 
			
		||||
		"@bull-board/ui": "^4.10.1",
 | 
			
		||||
		"@discordapp/twemoji": "14.0.2",
 | 
			
		||||
		"@fastify/accepts": "4.1.0",
 | 
			
		||||
		"@fastify/cookie": "^8.3.0",
 | 
			
		||||
@@ -38,10 +38,10 @@
 | 
			
		||||
		"@peertube/http-signature": "1.7.0",
 | 
			
		||||
		"@sinonjs/fake-timers": "10.0.2",
 | 
			
		||||
		"accepts": "^1.3.8",
 | 
			
		||||
		"ajv": "8.11.2",
 | 
			
		||||
		"ajv": "8.12.0",
 | 
			
		||||
		"archiver": "5.3.1",
 | 
			
		||||
		"autwh": "0.1.0",
 | 
			
		||||
		"aws-sdk": "2.1286.0",
 | 
			
		||||
		"aws-sdk": "2.1289.0",
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"blurhash": "2.0.4",
 | 
			
		||||
		"bull": "4.10.2",
 | 
			
		||||
@@ -109,8 +109,8 @@
 | 
			
		||||
		"stringz": "2.1.0",
 | 
			
		||||
		"summaly": "2.7.0",
 | 
			
		||||
		"syslog-pro": "git+https://github.com/misskey-dev/SyslogPro#0.2.9-misskey.2",
 | 
			
		||||
		"systeminformation": "5.16.9",
 | 
			
		||||
		"tinycolor2": "1.5.1",
 | 
			
		||||
		"systeminformation": "5.17.1",
 | 
			
		||||
		"tinycolor2": "1.5.2",
 | 
			
		||||
		"tmp": "0.2.1",
 | 
			
		||||
		"tsc-alias": "1.8.2",
 | 
			
		||||
		"tsconfig-paths": "4.1.2",
 | 
			
		||||
@@ -128,7 +128,7 @@
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@redocly/openapi-core": "1.0.0-beta.117",
 | 
			
		||||
		"@swc/core": "1.3.24",
 | 
			
		||||
		"@swc/core": "1.3.25",
 | 
			
		||||
		"@swc/jest": "0.2.24",
 | 
			
		||||
		"@types/accepts": "1.3.5",
 | 
			
		||||
		"@types/archiver": "5.3.1",
 | 
			
		||||
@@ -172,8 +172,8 @@
 | 
			
		||||
		"@types/web-push": "3.3.2",
 | 
			
		||||
		"@types/websocket": "1.0.5",
 | 
			
		||||
		"@types/ws": "8.5.4",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.47.1",
 | 
			
		||||
		"@typescript-eslint/parser": "5.47.1",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.48.0",
 | 
			
		||||
		"@typescript-eslint/parser": "5.48.0",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"eslint": "8.31.0",
 | 
			
		||||
		"eslint-plugin-import": "2.26.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -95,6 +95,8 @@ import { UserEntityService } from './entities/UserEntityService.js';
 | 
			
		||||
import { UserGroupEntityService } from './entities/UserGroupEntityService.js';
 | 
			
		||||
import { UserGroupInvitationEntityService } from './entities/UserGroupInvitationEntityService.js';
 | 
			
		||||
import { UserListEntityService } from './entities/UserListEntityService.js';
 | 
			
		||||
import { FlashEntityService } from './entities/FlashEntityService.js';
 | 
			
		||||
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
 | 
			
		||||
import { ApAudienceService } from './activitypub/ApAudienceService.js';
 | 
			
		||||
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
 | 
			
		||||
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
 | 
			
		||||
@@ -216,6 +218,8 @@ const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting
 | 
			
		||||
const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService };
 | 
			
		||||
const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService };
 | 
			
		||||
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
 | 
			
		||||
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
 | 
			
		||||
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
 | 
			
		||||
 | 
			
		||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
 | 
			
		||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
 | 
			
		||||
@@ -338,6 +342,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		UserGroupEntityService,
 | 
			
		||||
		UserGroupInvitationEntityService,
 | 
			
		||||
		UserListEntityService,
 | 
			
		||||
		FlashEntityService,
 | 
			
		||||
		FlashLikeEntityService,
 | 
			
		||||
		ApAudienceService,
 | 
			
		||||
		ApDbResolverService,
 | 
			
		||||
		ApDeliverManagerService,
 | 
			
		||||
@@ -455,6 +461,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$UserGroupEntityService,
 | 
			
		||||
		$UserGroupInvitationEntityService,
 | 
			
		||||
		$UserListEntityService,
 | 
			
		||||
		$FlashEntityService,
 | 
			
		||||
		$FlashLikeEntityService,
 | 
			
		||||
		$ApAudienceService,
 | 
			
		||||
		$ApDbResolverService,
 | 
			
		||||
		$ApDeliverManagerService,
 | 
			
		||||
@@ -572,6 +580,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		UserGroupEntityService,
 | 
			
		||||
		UserGroupInvitationEntityService,
 | 
			
		||||
		UserListEntityService,
 | 
			
		||||
		FlashEntityService,
 | 
			
		||||
		FlashLikeEntityService,
 | 
			
		||||
		ApAudienceService,
 | 
			
		||||
		ApDbResolverService,
 | 
			
		||||
		ApDeliverManagerService,
 | 
			
		||||
@@ -688,6 +698,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$UserGroupEntityService,
 | 
			
		||||
		$UserGroupInvitationEntityService,
 | 
			
		||||
		$UserListEntityService,
 | 
			
		||||
		$FlashEntityService,
 | 
			
		||||
		$FlashLikeEntityService,
 | 
			
		||||
		$ApAudienceService,
 | 
			
		||||
		$ApDbResolverService,
 | 
			
		||||
		$ApDeliverManagerService,
 | 
			
		||||
 
 | 
			
		||||
@@ -47,26 +47,6 @@ function truncateBody<T extends keyof pushNotificationsTypes>(type: T, body: pus
 | 
			
		||||
	return body;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function truncateUnreadAntennaNote(notification: pushNotificationsTypes['unreadAntennaNote']): pushNotificationsTypes['unreadAntennaNote'] {
 | 
			
		||||
	if (notification.note) {
 | 
			
		||||
		return {
 | 
			
		||||
			...notification,
 | 
			
		||||
			note: {
 | 
			
		||||
				...notification.note,
 | 
			
		||||
				// textをgetNoteSummaryしたものに置き換える
 | 
			
		||||
				text: getNoteSummary(('type' in notification && notification.type === 'renote') ? notification.note.renote as Packed<'Note'> : notification.note),
 | 
			
		||||
 | 
			
		||||
				cw: undefined,
 | 
			
		||||
				reply: undefined,
 | 
			
		||||
				renote: undefined,
 | 
			
		||||
				user: undefined as any, // 通知を受け取ったユーザーである場合が多いのでこれも捨てる アンテナの場合も不要なのでいらない
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return notification;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PushNotificationService {
 | 
			
		||||
	constructor(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								packages/backend/src/core/entities/FlashEntityService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								packages/backend/src/core/entities/FlashEntityService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
 | 
			
		||||
import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
			
		||||
import type { Packed } from '@/misc/schema.js';
 | 
			
		||||
import type { } from '@/models/entities/Blocking.js';
 | 
			
		||||
import type { User } from '@/models/entities/User.js';
 | 
			
		||||
import type { Flash } from '@/models/entities/Flash.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { UserEntityService } from './UserEntityService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class FlashEntityService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.flashLikesRepository)
 | 
			
		||||
		private flashLikesRepository: FlashLikesRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async pack(
 | 
			
		||||
		src: Flash['id'] | Flash,
 | 
			
		||||
		me?: { id: User['id'] } | null | undefined,
 | 
			
		||||
	): Promise<Packed<'Flash'>> {
 | 
			
		||||
		const meId = me ? me.id : null;
 | 
			
		||||
		const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return await awaitAll({
 | 
			
		||||
			id: flash.id,
 | 
			
		||||
			createdAt: flash.createdAt.toISOString(),
 | 
			
		||||
			updatedAt: flash.updatedAt.toISOString(),
 | 
			
		||||
			userId: flash.userId,
 | 
			
		||||
			user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意
 | 
			
		||||
			title: flash.title,
 | 
			
		||||
			summary: flash.summary,
 | 
			
		||||
			script: flash.script,
 | 
			
		||||
			likedCount: flash.likedCount,
 | 
			
		||||
			isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public packMany(
 | 
			
		||||
		flashs: Flash[],
 | 
			
		||||
		me?: { id: User['id'] } | null | undefined,
 | 
			
		||||
	) {
 | 
			
		||||
		return Promise.all(flashs.map(x => this.pack(x, me)));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								packages/backend/src/core/entities/FlashLikeEntityService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/backend/src/core/entities/FlashLikeEntityService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { FlashLikesRepository } from '@/models/index.js';
 | 
			
		||||
import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
			
		||||
import type { Packed } from '@/misc/schema.js';
 | 
			
		||||
import type { } from '@/models/entities/Blocking.js';
 | 
			
		||||
import type { User } from '@/models/entities/User.js';
 | 
			
		||||
import type { FlashLike } from '@/models/entities/FlashLike.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { UserEntityService } from './UserEntityService.js';
 | 
			
		||||
import { FlashEntityService } from './FlashEntityService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class FlashLikeEntityService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashLikesRepository)
 | 
			
		||||
		private flashLikesRepository: FlashLikesRepository,
 | 
			
		||||
 | 
			
		||||
		private flashEntityService: FlashEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async pack(
 | 
			
		||||
		src: FlashLike['id'] | FlashLike,
 | 
			
		||||
		me?: { id: User['id'] } | null | undefined,
 | 
			
		||||
	) {
 | 
			
		||||
		const like = typeof src === 'object' ? src : await this.flashLikesRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: like.id,
 | 
			
		||||
			flash: await this.flashEntityService.pack(like.flash ?? like.flashId, me),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public packMany(
 | 
			
		||||
		likes: any[],
 | 
			
		||||
		me: { id: User['id'] },
 | 
			
		||||
	) {
 | 
			
		||||
		return Promise.all(likes.map(x => this.pack(x, me)));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -69,5 +69,7 @@ export const DI = {
 | 
			
		||||
	adsRepository: Symbol('adsRepository'),
 | 
			
		||||
	passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
 | 
			
		||||
	retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
 | 
			
		||||
	flashsRepository: Symbol('flashsRepository'),
 | 
			
		||||
	flashLikesRepository: Symbol('flashLikesRepository'),
 | 
			
		||||
	//#endregion
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
 | 
			
		||||
const dictionary = {
 | 
			
		||||
	'safe-file': FILE_TYPE_BROWSERSAFE,
 | 
			
		||||
	'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/avif', 'image/svg+xml'],
 | 
			
		||||
	'sharp-animation-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation } from './index.js';
 | 
			
		||||
import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash } from './index.js';
 | 
			
		||||
import type { DataSource } from 'typeorm';
 | 
			
		||||
import type { Provider } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
@@ -388,6 +388,18 @@ const $retentionAggregationsRepository: Provider = {
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $flashsRepository: Provider = {
 | 
			
		||||
	provide: DI.flashsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(Flash),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $flashLikesRepository: Provider = {
 | 
			
		||||
	provide: DI.flashLikesRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(FlashLike),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
	imports: [
 | 
			
		||||
	],
 | 
			
		||||
@@ -456,6 +468,8 @@ const $retentionAggregationsRepository: Provider = {
 | 
			
		||||
		$adsRepository,
 | 
			
		||||
		$passwordResetRequestsRepository,
 | 
			
		||||
		$retentionAggregationsRepository,
 | 
			
		||||
		$flashsRepository,
 | 
			
		||||
		$flashLikesRepository,
 | 
			
		||||
	],
 | 
			
		||||
	exports: [
 | 
			
		||||
		$usersRepository,
 | 
			
		||||
@@ -522,6 +536,8 @@ const $retentionAggregationsRepository: Provider = {
 | 
			
		||||
		$adsRepository,
 | 
			
		||||
		$passwordResetRequestsRepository,
 | 
			
		||||
		$retentionAggregationsRepository,
 | 
			
		||||
		$flashsRepository,
 | 
			
		||||
		$flashLikesRepository,
 | 
			
		||||
	],
 | 
			
		||||
})
 | 
			
		||||
export class RepositoryModule {}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								packages/backend/src/models/entities/Flash.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								packages/backend/src/models/entities/Flash.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from '../id.js';
 | 
			
		||||
import { User } from './User.js';
 | 
			
		||||
import { DriveFile } from './DriveFile.js';
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
export class Flash {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		comment: 'The created date of the Flash.',
 | 
			
		||||
	})
 | 
			
		||||
	public createdAt: Date;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		comment: 'The updated date of the Flash.',
 | 
			
		||||
	})
 | 
			
		||||
	public updatedAt: Date;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256,
 | 
			
		||||
	})
 | 
			
		||||
	public title: string;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024,
 | 
			
		||||
	})
 | 
			
		||||
	public summary: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		comment: 'The ID of author.',
 | 
			
		||||
	})
 | 
			
		||||
	public userId: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: User | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 16384,
 | 
			
		||||
	})
 | 
			
		||||
	public script: string;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256, array: true, default: '{}',
 | 
			
		||||
	})
 | 
			
		||||
	public permissions: string[];
 | 
			
		||||
 | 
			
		||||
	@Column('integer', {
 | 
			
		||||
		default: 0,
 | 
			
		||||
	})
 | 
			
		||||
	public likedCount: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								packages/backend/src/models/entities/FlashLike.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packages/backend/src/models/entities/FlashLike.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from '../id.js';
 | 
			
		||||
import { User } from './User.js';
 | 
			
		||||
import { Flash } from './Flash.js';
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
@Index(['userId', 'flashId'], { unique: true })
 | 
			
		||||
export class FlashLike {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Column('timestamp with time zone')
 | 
			
		||||
	public createdAt: Date;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public userId: User['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: User | null;
 | 
			
		||||
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public flashId: Flash['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => Flash, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public flash: Flash | null;
 | 
			
		||||
}
 | 
			
		||||
@@ -62,6 +62,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
 | 
			
		||||
import { Webhook } from '@/models/entities/Webhook.js';
 | 
			
		||||
import { Channel } from '@/models/entities/Channel.js';
 | 
			
		||||
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
 | 
			
		||||
import { Flash } from '@/models/entities/Flash.js';
 | 
			
		||||
import { FlashLike } from '@/models/entities/FlashLike.js';
 | 
			
		||||
import type { Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
@@ -129,6 +131,8 @@ export {
 | 
			
		||||
	Webhook,
 | 
			
		||||
	Channel,
 | 
			
		||||
	RetentionAggregation,
 | 
			
		||||
	Flash,
 | 
			
		||||
	FlashLike,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type AbuseUserReportsRepository = Repository<AbuseUserReport>;
 | 
			
		||||
@@ -195,3 +199,5 @@ export type UserSecurityKeysRepository = Repository<UserSecurityKey>;
 | 
			
		||||
export type WebhooksRepository = Repository<Webhook>;
 | 
			
		||||
export type ChannelsRepository = Repository<Channel>;
 | 
			
		||||
export type RetentionAggregationsRepository = Repository<RetentionAggregation>;
 | 
			
		||||
export type FlashsRepository = Repository<Flash>;
 | 
			
		||||
export type FlashLikesRepository = Repository<FlashLike>;
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
 | 
			
		||||
import { Webhook } from '@/models/entities/Webhook.js';
 | 
			
		||||
import { Channel } from '@/models/entities/Channel.js';
 | 
			
		||||
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
 | 
			
		||||
import { Flash } from '@/models/entities/Flash.js';
 | 
			
		||||
import { FlashLike } from '@/models/entities/FlashLike.js';
 | 
			
		||||
 | 
			
		||||
import { Config } from '@/config.js';
 | 
			
		||||
import MisskeyLogger from '@/logger.js';
 | 
			
		||||
@@ -184,6 +186,8 @@ export const entities = [
 | 
			
		||||
	Webhook,
 | 
			
		||||
	UserIp,
 | 
			
		||||
	RetentionAggregation,
 | 
			
		||||
	Flash,
 | 
			
		||||
	FlashLike,
 | 
			
		||||
	...charts,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -79,10 +79,18 @@ export class MediaProxyServerService {
 | 
			
		||||
	
 | 
			
		||||
			const { mime, ext } = await this.fileInfoService.detectType(path);
 | 
			
		||||
			const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
 | 
			
		||||
			const isAnimationConvertibleImage = isMimeImage(mime, 'sharp-animation-convertible-image');
 | 
			
		||||
	
 | 
			
		||||
			let image: IImage;
 | 
			
		||||
			if ('emoji' in request.query && isConvertibleImage) {
 | 
			
		||||
				const data = await sharp(path, { animated: !('static' in request.query) })
 | 
			
		||||
				if (!isAnimationConvertibleImage && !('static' in request.query)) {
 | 
			
		||||
					image = {
 | 
			
		||||
						data: fs.readFileSync(path),
 | 
			
		||||
						ext,
 | 
			
		||||
						type: mime,
 | 
			
		||||
					};
 | 
			
		||||
				} else {
 | 
			
		||||
					const data = await sharp(path, { animated: !('static' in request.query) })
 | 
			
		||||
					.resize({
 | 
			
		||||
						height: 128,
 | 
			
		||||
						withoutEnlargement: true,
 | 
			
		||||
@@ -90,11 +98,12 @@ export class MediaProxyServerService {
 | 
			
		||||
					.webp(webpDefault)
 | 
			
		||||
					.toBuffer();
 | 
			
		||||
 | 
			
		||||
				image = {
 | 
			
		||||
					data,
 | 
			
		||||
					ext: 'webp',
 | 
			
		||||
					type: 'image/webp',
 | 
			
		||||
				};
 | 
			
		||||
					image = {
 | 
			
		||||
						data,
 | 
			
		||||
						ext: 'webp',
 | 
			
		||||
						type: 'image/webp',
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
			} else if ('static' in request.query && isConvertibleImage) {
 | 
			
		||||
				image = await this.imageProcessingService.convertToWebp(path, 498, 280);
 | 
			
		||||
			} else if ('preview' in request.query && isConvertibleImage) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,11 @@
 | 
			
		||||
import cluster from 'node:cluster';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import * as http from 'node:http';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import Fastify from 'fastify';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { envOption } from '@/env.js';
 | 
			
		||||
@@ -39,6 +38,9 @@ export class ServerService {
 | 
			
		||||
		@Inject(DI.userProfilesRepository)
 | 
			
		||||
		private userProfilesRepository: UserProfilesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.emojisRepository)
 | 
			
		||||
		private emojisRepository: EmojisRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private apiServerService: ApiServerService,
 | 
			
		||||
		private streamingApiServerService: StreamingApiServerService,
 | 
			
		||||
@@ -77,6 +79,43 @@ export class ServerService {
 | 
			
		||||
		fastify.register(this.nodeinfoServerService.createServer);
 | 
			
		||||
		fastify.register(this.wellKnownServerService.createServer);
 | 
			
		||||
 | 
			
		||||
		fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
 | 
			
		||||
			const path = request.params.path;
 | 
			
		||||
 | 
			
		||||
			if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
 | 
			
		||||
				reply.code(404);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=86400');
 | 
			
		||||
 | 
			
		||||
			const name = path.split('@')[0].replace('.webp', '');
 | 
			
		||||
			const host = path.split('@')[1]?.replace('.webp', '');
 | 
			
		||||
 | 
			
		||||
			const emoji = await this.emojisRepository.findOneBy({
 | 
			
		||||
				// `@.` is the spec of ReactionService.decodeReaction
 | 
			
		||||
				host: (host == null || host === '.') ? IsNull() : host,
 | 
			
		||||
				name: name,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
 | 
			
		||||
 | 
			
		||||
			if (emoji == null) {
 | 
			
		||||
				return await reply.redirect('/static-assets/emoji-unknown.png');
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const url = new URL('/proxy/emoji.webp', this.config.url);
 | 
			
		||||
			// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
 | 
			
		||||
			url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
 | 
			
		||||
			url.searchParams.set('emoji', '1');
 | 
			
		||||
			if ('static' in request.query) url.searchParams.set('static', '1');
 | 
			
		||||
 | 
			
		||||
			return await reply.redirect(
 | 
			
		||||
				301,
 | 
			
		||||
				url.toString(),
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.get<{ Params: { acct: string } }>('/avatar/@:acct', async (request, reply) => {
 | 
			
		||||
			const { username, host } = Acct.parse(request.params.acct);
 | 
			
		||||
			const user = await this.usersRepository.findOne({
 | 
			
		||||
 
 | 
			
		||||
@@ -266,6 +266,15 @@ import * as ep___pages_like from './endpoints/pages/like.js';
 | 
			
		||||
import * as ep___pages_show from './endpoints/pages/show.js';
 | 
			
		||||
import * as ep___pages_unlike from './endpoints/pages/unlike.js';
 | 
			
		||||
import * as ep___pages_update from './endpoints/pages/update.js';
 | 
			
		||||
import * as ep___flash_create from './endpoints/flash/create.js';
 | 
			
		||||
import * as ep___flash_delete from './endpoints/flash/delete.js';
 | 
			
		||||
import * as ep___flash_featured from './endpoints/flash/featured.js';
 | 
			
		||||
import * as ep___flash_like from './endpoints/flash/like.js';
 | 
			
		||||
import * as ep___flash_show from './endpoints/flash/show.js';
 | 
			
		||||
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
 | 
			
		||||
import * as ep___flash_update from './endpoints/flash/update.js';
 | 
			
		||||
import * as ep___flash_my from './endpoints/flash/my.js';
 | 
			
		||||
import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
 | 
			
		||||
import * as ep___ping from './endpoints/ping.js';
 | 
			
		||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
 | 
			
		||||
import * as ep___promo_read from './endpoints/promo/read.js';
 | 
			
		||||
@@ -587,6 +596,15 @@ const $pages_like: Provider = { provide: 'ep:pages/like', useClass: ep___pages_l
 | 
			
		||||
const $pages_show: Provider = { provide: 'ep:pages/show', useClass: ep___pages_show.default };
 | 
			
		||||
const $pages_unlike: Provider = { provide: 'ep:pages/unlike', useClass: ep___pages_unlike.default };
 | 
			
		||||
const $pages_update: Provider = { provide: 'ep:pages/update', useClass: ep___pages_update.default };
 | 
			
		||||
const $flash_create: Provider = { provide: 'ep:flash/create', useClass: ep___flash_create.default };
 | 
			
		||||
const $flash_delete: Provider = { provide: 'ep:flash/delete', useClass: ep___flash_delete.default };
 | 
			
		||||
const $flash_featured: Provider = { provide: 'ep:flash/featured', useClass: ep___flash_featured.default };
 | 
			
		||||
const $flash_like: Provider = { provide: 'ep:flash/like', useClass: ep___flash_like.default };
 | 
			
		||||
const $flash_show: Provider = { provide: 'ep:flash/show', useClass: ep___flash_show.default };
 | 
			
		||||
const $flash_unlike: Provider = { provide: 'ep:flash/unlike', useClass: ep___flash_unlike.default };
 | 
			
		||||
const $flash_update: Provider = { provide: 'ep:flash/update', useClass: ep___flash_update.default };
 | 
			
		||||
const $flash_my: Provider = { provide: 'ep:flash/my', useClass: ep___flash_my.default };
 | 
			
		||||
const $flash_myLikes: Provider = { provide: 'ep:flash/my-likes', useClass: ep___flash_myLikes.default };
 | 
			
		||||
const $ping: Provider = { provide: 'ep:ping', useClass: ep___ping.default };
 | 
			
		||||
const $pinnedUsers: Provider = { provide: 'ep:pinned-users', useClass: ep___pinnedUsers.default };
 | 
			
		||||
const $promo_read: Provider = { provide: 'ep:promo/read', useClass: ep___promo_read.default };
 | 
			
		||||
@@ -912,6 +930,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		||||
		$pages_show,
 | 
			
		||||
		$pages_unlike,
 | 
			
		||||
		$pages_update,
 | 
			
		||||
		$flash_create,
 | 
			
		||||
		$flash_delete,
 | 
			
		||||
		$flash_featured,
 | 
			
		||||
		$flash_like,
 | 
			
		||||
		$flash_show,
 | 
			
		||||
		$flash_unlike,
 | 
			
		||||
		$flash_update,
 | 
			
		||||
		$flash_my,
 | 
			
		||||
		$flash_myLikes,
 | 
			
		||||
		$ping,
 | 
			
		||||
		$pinnedUsers,
 | 
			
		||||
		$promo_read,
 | 
			
		||||
@@ -1231,6 +1258,15 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 | 
			
		||||
		$pages_show,
 | 
			
		||||
		$pages_unlike,
 | 
			
		||||
		$pages_update,
 | 
			
		||||
		$flash_create,
 | 
			
		||||
		$flash_delete,
 | 
			
		||||
		$flash_featured,
 | 
			
		||||
		$flash_like,
 | 
			
		||||
		$flash_show,
 | 
			
		||||
		$flash_unlike,
 | 
			
		||||
		$flash_update,
 | 
			
		||||
		$flash_my,
 | 
			
		||||
		$flash_myLikes,
 | 
			
		||||
		$ping,
 | 
			
		||||
		$pinnedUsers,
 | 
			
		||||
		$promo_read,
 | 
			
		||||
 
 | 
			
		||||
@@ -265,6 +265,15 @@ import * as ep___pages_like from './endpoints/pages/like.js';
 | 
			
		||||
import * as ep___pages_show from './endpoints/pages/show.js';
 | 
			
		||||
import * as ep___pages_unlike from './endpoints/pages/unlike.js';
 | 
			
		||||
import * as ep___pages_update from './endpoints/pages/update.js';
 | 
			
		||||
import * as ep___flash_create from './endpoints/flash/create.js';
 | 
			
		||||
import * as ep___flash_delete from './endpoints/flash/delete.js';
 | 
			
		||||
import * as ep___flash_featured from './endpoints/flash/featured.js';
 | 
			
		||||
import * as ep___flash_like from './endpoints/flash/like.js';
 | 
			
		||||
import * as ep___flash_show from './endpoints/flash/show.js';
 | 
			
		||||
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
 | 
			
		||||
import * as ep___flash_update from './endpoints/flash/update.js';
 | 
			
		||||
import * as ep___flash_my from './endpoints/flash/my.js';
 | 
			
		||||
import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
 | 
			
		||||
import * as ep___ping from './endpoints/ping.js';
 | 
			
		||||
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
 | 
			
		||||
import * as ep___promo_read from './endpoints/promo/read.js';
 | 
			
		||||
@@ -584,6 +593,15 @@ const eps = [
 | 
			
		||||
	['pages/show', ep___pages_show],
 | 
			
		||||
	['pages/unlike', ep___pages_unlike],
 | 
			
		||||
	['pages/update', ep___pages_update],
 | 
			
		||||
	['flash/create', ep___flash_create],
 | 
			
		||||
	['flash/delete', ep___flash_delete],
 | 
			
		||||
	['flash/featured', ep___flash_featured],
 | 
			
		||||
	['flash/like', ep___flash_like],
 | 
			
		||||
	['flash/show', ep___flash_show],
 | 
			
		||||
	['flash/unlike', ep___flash_unlike],
 | 
			
		||||
	['flash/update', ep___flash_update],
 | 
			
		||||
	['flash/my', ep___flash_my],
 | 
			
		||||
	['flash/my-likes', ep___flash_myLikes],
 | 
			
		||||
	['ping', ep___ping],
 | 
			
		||||
	['pinned-users', ep___pinnedUsers],
 | 
			
		||||
	['promo/read', ep___promo_read],
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: false,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requireModerator: true,
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -64,8 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
				case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
 | 
			
		||||
				case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
 | 
			
		||||
				case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
 | 
			
		||||
				case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC'); break;
 | 
			
		||||
				case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC'); break;
 | 
			
		||||
				case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC', 'NULLS LAST'); break;
 | 
			
		||||
				case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC', 'NULLS FIRST'); break;
 | 
			
		||||
 | 
			
		||||
				default: query.orderBy('instance.id', 'DESC'); break;
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								packages/backend/src/server/api/endpoints/flash/create.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								packages/backend/src/server/api/endpoints/flash/create.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { DriveFilesRepository, FlashsRepository, PagesRepository } from '@/models/index.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { Page } from '@/models/entities/Page.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { PageEntityService } from '@/core/entities/PageEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:flash',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 10,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		title: { type: 'string' },
 | 
			
		||||
		summary: { type: 'string' },
 | 
			
		||||
		script: { type: 'string' },
 | 
			
		||||
		permissions: { type: 'array', items: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
		} },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['title', 'summary', 'script', 'permissions'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		private flashEntityService: FlashEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const flash = await this.flashsRepository.insert({
 | 
			
		||||
				id: this.idService.genId(),
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
				createdAt: new Date(),
 | 
			
		||||
				updatedAt: new Date(),
 | 
			
		||||
				title: ps.title,
 | 
			
		||||
				summary: ps.summary,
 | 
			
		||||
				script: ps.script,
 | 
			
		||||
				permissions: ps.permissions,
 | 
			
		||||
			}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
			return await this.flashEntityService.pack(flash);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								packages/backend/src/server/api/endpoints/flash/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								packages/backend/src/server/api/endpoints/flash/delete.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { FlashsRepository } from '@/models/index.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flashs'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:flash',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchFlash: {
 | 
			
		||||
			message: 'No such flash.',
 | 
			
		||||
			code: 'NO_SUCH_FLASH',
 | 
			
		||||
			id: 'de1623ef-bbb3-4289-a71e-14cfa83d9740',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accessDenied: {
 | 
			
		||||
			message: 'Access denied.',
 | 
			
		||||
			code: 'ACCESS_DENIED',
 | 
			
		||||
			id: '1036ad7b-9f92-4fff-89c3-0e50dc941704',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		flashId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['flashId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
 | 
			
		||||
			if (flash == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchFlash);
 | 
			
		||||
			}
 | 
			
		||||
			if (flash.userId !== me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.accessDenied);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.flashsRepository.delete(flash.id);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								packages/backend/src/server/api/endpoints/flash/featured.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								packages/backend/src/server/api/endpoints/flash/featured.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { FlashsRepository } from '@/models/index.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: false,
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'Flash',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {},
 | 
			
		||||
	required: [],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		private flashEntityService: FlashEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const query = this.flashsRepository.createQueryBuilder('flash')
 | 
			
		||||
				.andWhere('flash.likedCount > 0')
 | 
			
		||||
				.orderBy('flash.likedCount', 'DESC');
 | 
			
		||||
 | 
			
		||||
			const flashs = await query.take(10).getMany();
 | 
			
		||||
 | 
			
		||||
			return await this.flashEntityService.packMany(flashs, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								packages/backend/src/server/api/endpoints/flash/like.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								packages/backend/src/server/api/endpoints/flash/like.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:flash-likes',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchFlash: {
 | 
			
		||||
			message: 'No such flash.',
 | 
			
		||||
			code: 'NO_SUCH_FLASH',
 | 
			
		||||
			id: 'c07c1491-9161-4c5c-9d75-01906f911f73',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		yourFlash: {
 | 
			
		||||
			message: 'You cannot like your flash.',
 | 
			
		||||
			code: 'YOUR_FLASH',
 | 
			
		||||
			id: '3fd8a0e7-5955-4ba9-85bb-bf3e0c30e13b',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		alreadyLiked: {
 | 
			
		||||
			message: 'The flash has already been liked.',
 | 
			
		||||
			code: 'ALREADY_LIKED',
 | 
			
		||||
			id: '010065cf-ad43-40df-8067-abff9f4686e3',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		flashId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['flashId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.flashLikesRepository)
 | 
			
		||||
		private flashLikesRepository: FlashLikesRepository,
 | 
			
		||||
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
 | 
			
		||||
			if (flash == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchFlash);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (flash.userId === me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.yourFlash);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// if already liked
 | 
			
		||||
			const exist = await this.flashLikesRepository.findOneBy({
 | 
			
		||||
				flashId: flash.id,
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (exist != null) {
 | 
			
		||||
				throw new ApiError(meta.errors.alreadyLiked);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Create like
 | 
			
		||||
			await this.flashLikesRepository.insert({
 | 
			
		||||
				id: this.idService.genId(),
 | 
			
		||||
				createdAt: new Date(),
 | 
			
		||||
				flashId: flash.id,
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.flashsRepository.increment({ id: flash.id }, 'likedCount', 1);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								packages/backend/src/server/api/endpoints/flash/my-likes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								packages/backend/src/server/api/endpoints/flash/my-likes.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { FlashLikesRepository } from '@/models/index.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['account', 'flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:flash-likes',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			properties: {
 | 
			
		||||
				id: {
 | 
			
		||||
					type: 'string',
 | 
			
		||||
					optional: false, nullable: false,
 | 
			
		||||
					format: 'id',
 | 
			
		||||
				},
 | 
			
		||||
				flash: {
 | 
			
		||||
					type: 'object',
 | 
			
		||||
					optional: false, nullable: false,
 | 
			
		||||
					ref: 'Flash',
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: [],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashLikesRepository)
 | 
			
		||||
		private flashLikesRepository: FlashLikesRepository,
 | 
			
		||||
 | 
			
		||||
		private flashLikeEntityService: FlashLikeEntityService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId)
 | 
			
		||||
				.andWhere('like.userId = :meId', { meId: me.id })
 | 
			
		||||
				.leftJoinAndSelect('like.flash', 'flash');
 | 
			
		||||
 | 
			
		||||
			const likes = await query
 | 
			
		||||
				.take(ps.limit)
 | 
			
		||||
				.getMany();
 | 
			
		||||
 | 
			
		||||
			return this.flashLikeEntityService.packMany(likes, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								packages/backend/src/server/api/endpoints/flash/my.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								packages/backend/src/server/api/endpoints/flash/my.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { FlashsRepository } from '@/models/index.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['account', 'flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:flash',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'Flash',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: [],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		private flashEntityService: FlashEntityService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId)
 | 
			
		||||
				.andWhere('flash.userId = :meId', { meId: me.id });
 | 
			
		||||
 | 
			
		||||
			const flashs = await query
 | 
			
		||||
				.take(ps.limit)
 | 
			
		||||
				.getMany();
 | 
			
		||||
 | 
			
		||||
			return await this.flashEntityService.packMany(flashs);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								packages/backend/src/server/api/endpoints/flash/show.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								packages/backend/src/server/api/endpoints/flash/show.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { UsersRepository, FlashsRepository } from '@/models/index.js';
 | 
			
		||||
import type { Flash } from '@/models/entities/Flash.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flashs'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: false,
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'Flash',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchFlash: {
 | 
			
		||||
			message: 'No such flash.',
 | 
			
		||||
			code: 'NO_SUCH_FLASH',
 | 
			
		||||
			id: 'f0d34a1a-d29a-401d-90ba-1982122b5630',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		flashId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['flashId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		private flashEntityService: FlashEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
 | 
			
		||||
 | 
			
		||||
			if (flash == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchFlash);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await this.flashEntityService.pack(flash, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								packages/backend/src/server/api/endpoints/flash/unlike.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								packages/backend/src/server/api/endpoints/flash/unlike.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { FlashsRepository, FlashLikesRepository } from '@/models/index.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:flash-likes',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchFlash: {
 | 
			
		||||
			message: 'No such flash.',
 | 
			
		||||
			code: 'NO_SUCH_FLASH',
 | 
			
		||||
			id: 'afe8424a-a69e-432d-a5f2-2f0740c62410',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		notLiked: {
 | 
			
		||||
			message: 'You have not liked that flash.',
 | 
			
		||||
			code: 'NOT_LIKED',
 | 
			
		||||
			id: '755f25a7-9871-4f65-9f34-51eaad9ae0ac',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		flashId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['flashId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.flashLikesRepository)
 | 
			
		||||
		private flashLikesRepository: FlashLikesRepository,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
 | 
			
		||||
			if (flash == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchFlash);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const exist = await this.flashLikesRepository.findOneBy({
 | 
			
		||||
				flashId: flash.id,
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (exist == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.notLiked);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Delete like
 | 
			
		||||
			await this.flashLikesRepository.delete(exist.id);
 | 
			
		||||
 | 
			
		||||
			this.flashsRepository.decrement({ id: flash.id }, 'likedCount', 1);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								packages/backend/src/server/api/endpoints/flash/update.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								packages/backend/src/server/api/endpoints/flash/update.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Not } from 'typeorm';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { FlashsRepository, DriveFilesRepository } from '@/models/index.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['flash'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:flash',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 300,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchFlash: {
 | 
			
		||||
			message: 'No such flash.',
 | 
			
		||||
			code: 'NO_SUCH_FLASH',
 | 
			
		||||
			id: '611e13d2-309e-419a-a5e4-e0422da39b02',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		accessDenied: {
 | 
			
		||||
			message: 'Access denied.',
 | 
			
		||||
			code: 'ACCESS_DENIED',
 | 
			
		||||
			id: '08e60c88-5948-478e-a132-02ec701d67b2',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		flashId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		title: { type: 'string' },
 | 
			
		||||
		summary: { type: 'string' },
 | 
			
		||||
		script: { type: 'string' },
 | 
			
		||||
		permissions: { type: 'array', items: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
		} },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['flashId', 'title', 'summary', 'script', 'permissions'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });
 | 
			
		||||
			if (flash == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchFlash);
 | 
			
		||||
			}
 | 
			
		||||
			if (flash.userId !== me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.accessDenied);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			await this.flashsRepository.update(flash.id, {
 | 
			
		||||
				updatedAt: new Date(),
 | 
			
		||||
				title: ps.title,
 | 
			
		||||
				summary: ps.summary,
 | 
			
		||||
				script: ps.script,
 | 
			
		||||
				permissions: ps.permissions,
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -28,7 +28,7 @@ export const meta = {
 | 
			
		||||
		alreadyLiked: {
 | 
			
		||||
			message: 'The page has already been liked.',
 | 
			
		||||
			code: 'ALREADY_LIKED',
 | 
			
		||||
			id: 'cc98a8a2-0dc3-4123-b198-62c71df18ed3',
 | 
			
		||||
			id: 'd4c1edbe-7da2-4eae-8714-1acfd2d63941',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
			
		||||
				updatedAt: new Date(),
 | 
			
		||||
				title: ps.title,
 | 
			
		||||
				name: ps.name === undefined ? page.name : ps.name,
 | 
			
		||||
				summary: ps.name === undefined ? page.summary : ps.summary,
 | 
			
		||||
				summary: ps.summary === undefined ? page.summary : ps.summary,
 | 
			
		||||
				content: ps.content,
 | 
			
		||||
				variables: ps.variables,
 | 
			
		||||
				script: ps.script,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { createBullBoard } from '@bull-board/api';
 | 
			
		||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
 | 
			
		||||
@@ -26,9 +25,10 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
 | 
			
		||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
 | 
			
		||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
 | 
			
		||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
 | 
			
		||||
import type { ChannelsRepository, ClipsRepository, EmojisRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
 | 
			
		||||
import { deepClone } from '@/misc/clone.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
 | 
			
		||||
import manifest from './manifest.json' assert { type: 'json' };
 | 
			
		||||
import { FeedService } from './FeedService.js';
 | 
			
		||||
import { UrlPreviewService } from './UrlPreviewService.js';
 | 
			
		||||
@@ -70,9 +70,10 @@ export class ClientServerService {
 | 
			
		||||
		@Inject(DI.pagesRepository)
 | 
			
		||||
		private pagesRepository: PagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.emojisRepository)
 | 
			
		||||
		private emojisRepository: EmojisRepository,
 | 
			
		||||
		@Inject(DI.flashsRepository)
 | 
			
		||||
		private flashsRepository: FlashsRepository,
 | 
			
		||||
 | 
			
		||||
		private flashEntityService: FlashEntityService,
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private pageEntityService: PageEntityService,
 | 
			
		||||
@@ -220,44 +221,6 @@ export class ClientServerService {
 | 
			
		||||
			return reply.sendFile('/apple-touch-icon.png', staticAssets);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.get<{ Params: { path: string }; Querystring: { static?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
 | 
			
		||||
			const path = request.params.path;
 | 
			
		||||
 | 
			
		||||
			if (!path.match(/^[a-zA-Z0-9\-_@\.]+?\.webp$/)) {
 | 
			
		||||
				reply.code(404);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			reply.header('Cache-Control', 'public, max-age=86400');
 | 
			
		||||
 | 
			
		||||
			const name = path.split('@')[0].replace('.webp', '');
 | 
			
		||||
			const host = path.split('@')[1]?.replace('.webp', '');
 | 
			
		||||
 | 
			
		||||
			const emoji = await this.emojisRepository.findOneBy({
 | 
			
		||||
				// `@.` is the spec of ReactionService.decodeReaction
 | 
			
		||||
				host: (host == null || host === '.') ? IsNull() : host,
 | 
			
		||||
				name: name,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (emoji == null) {
 | 
			
		||||
				reply.code(404);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
 | 
			
		||||
 | 
			
		||||
			const url = new URL('/proxy/emoji.webp', this.config.url);
 | 
			
		||||
			// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
 | 
			
		||||
			url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
 | 
			
		||||
			url.searchParams.set('emoji', '1');
 | 
			
		||||
			if ('static' in request.query) url.searchParams.set('static', '1');
 | 
			
		||||
 | 
			
		||||
			return await reply.redirect(
 | 
			
		||||
				301,
 | 
			
		||||
				url.toString(),
 | 
			
		||||
			);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
 | 
			
		||||
			const path = request.params.path;
 | 
			
		||||
 | 
			
		||||
@@ -352,7 +315,7 @@ export class ClientServerService {
 | 
			
		||||
			const name = meta.name || 'Misskey';
 | 
			
		||||
			let content = '';
 | 
			
		||||
			content += '<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">';
 | 
			
		||||
			content += `<ShortName>${name} Search</ShortName>`;
 | 
			
		||||
			content += `<ShortName>${name}</ShortName>`;
 | 
			
		||||
			content += `<Description>${name} Search</Description>`;
 | 
			
		||||
			content += '<InputEncoding>UTF-8</InputEncoding>';
 | 
			
		||||
			content += `<Image width="16" height="16" type="image/x-icon">${this.config.url}/favicon.ico</Image>`;
 | 
			
		||||
@@ -545,6 +508,30 @@ export class ClientServerService {
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Flash
 | 
			
		||||
		fastify.get<{ Params: { id: string; } }>('/play/:id', async (request, reply) => {
 | 
			
		||||
			const flash = await this.flashsRepository.findOneBy({
 | 
			
		||||
				id: request.params.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (flash) {
 | 
			
		||||
				const _flash = await this.flashEntityService.pack(flash);
 | 
			
		||||
				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
 | 
			
		||||
				const meta = await this.metaService.fetch();
 | 
			
		||||
				reply.header('Cache-Control', 'public, max-age=15');
 | 
			
		||||
				return await reply.view('flash', {
 | 
			
		||||
					flash: _flash,
 | 
			
		||||
					profile,
 | 
			
		||||
					avatarUrl: await this.userEntityService.getAvatarUrl(await this.usersRepository.findOneByOrFail({ id: flash.userId })),
 | 
			
		||||
					instanceName: meta.name ?? 'Misskey',
 | 
			
		||||
					icon: meta.iconUrl,
 | 
			
		||||
					themeColor: meta.themeColor,
 | 
			
		||||
				});
 | 
			
		||||
			} else {
 | 
			
		||||
				return await renderBase(reply);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Clip
 | 
			
		||||
		// TODO: 非publicなclipのハンドリング
 | 
			
		||||
		fastify.get<{ Params: { clip: string; } }>('/clips/:clip', async (request, reply) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ html
 | 
			
		||||
		link(rel='icon' href= icon || '/favicon.ico')
 | 
			
		||||
		link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
 | 
			
		||||
		link(rel='manifest' href='/manifest.json')
 | 
			
		||||
		link(rel='search' type='application/opensearchdescription+xml' title=((title || "Misskey") + " Search") href=`${url}/opensearch.xml`)
 | 
			
		||||
		link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
 | 
			
		||||
		link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
 | 
			
		||||
		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
 | 
			
		||||
		link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								packages/backend/src/server/web/views/flash.pug
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/backend/src/server/web/views/flash.pug
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
extends ./base
 | 
			
		||||
 | 
			
		||||
block vars
 | 
			
		||||
	- const user = flash.user;
 | 
			
		||||
	- const title = flash.title;
 | 
			
		||||
	- const url = `${config.url}/play/${flash.id}`;
 | 
			
		||||
 | 
			
		||||
block title
 | 
			
		||||
	= `${title} | ${instanceName}`
 | 
			
		||||
 | 
			
		||||
block desc
 | 
			
		||||
	meta(name='description' content= flash.summary)
 | 
			
		||||
 | 
			
		||||
block og
 | 
			
		||||
	meta(property='og:type'        content='article')
 | 
			
		||||
	meta(property='og:title'       content= title)
 | 
			
		||||
	meta(property='og:description' content= flash.summary)
 | 
			
		||||
	meta(property='og:url'         content= url)
 | 
			
		||||
	meta(property='og:image'       content= avatarUrl)
 | 
			
		||||
 | 
			
		||||
block meta
 | 
			
		||||
	if profile.noCrawle
 | 
			
		||||
		meta(name='robots' content='noindex')
 | 
			
		||||
 | 
			
		||||
	meta(name='misskey:user-username' content=user.username)
 | 
			
		||||
	meta(name='misskey:user-id' content=user.id)
 | 
			
		||||
	meta(name='misskey:flash-id' content=flash.id)
 | 
			
		||||
 | 
			
		||||
	// todo
 | 
			
		||||
	if user.twitter
 | 
			
		||||
		meta(name='twitter:creator' content=`@${user.twitter.screenName}`)
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
		"@rollup/plugin-alias": "4.0.2",
 | 
			
		||||
		"@rollup/plugin-json": "6.0.0",
 | 
			
		||||
		"@rollup/pluginutils": "5.0.2",
 | 
			
		||||
		"@syuilo/aiscript": "0.12.0",
 | 
			
		||||
		"@syuilo/aiscript": "0.12.2",
 | 
			
		||||
		"@tabler/icons": "^1.118.0",
 | 
			
		||||
		"@vitejs/plugin-vue": "4.0.0",
 | 
			
		||||
		"@vue/compiler-sfc": "3.2.45",
 | 
			
		||||
@@ -20,7 +20,8 @@
 | 
			
		||||
		"blurhash": "2.0.4",
 | 
			
		||||
		"broadcast-channel": "4.19.1",
 | 
			
		||||
		"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
 | 
			
		||||
		"chart.js": "4.1.1",
 | 
			
		||||
		"canvas-confetti": "^1.6.0",
 | 
			
		||||
		"chart.js": "4.1.2",
 | 
			
		||||
		"chartjs-adapter-date-fns": "3.0.0",
 | 
			
		||||
		"chartjs-chart-matrix": "^1.3.0",
 | 
			
		||||
		"chartjs-plugin-gradient": "0.6.1",
 | 
			
		||||
@@ -35,7 +36,7 @@
 | 
			
		||||
		"insert-text-at-cursor": "0.3.0",
 | 
			
		||||
		"is-file-animated": "1.0.2",
 | 
			
		||||
		"json5": "2.2.3",
 | 
			
		||||
		"katex": "0.15.6",
 | 
			
		||||
		"katex": "0.16.4",
 | 
			
		||||
		"matter-js": "0.18.0",
 | 
			
		||||
		"mfm-js": "0.23.0",
 | 
			
		||||
		"misskey-js": "0.0.14",
 | 
			
		||||
@@ -44,7 +45,7 @@
 | 
			
		||||
		"punycode": "2.1.1",
 | 
			
		||||
		"querystring": "0.2.1",
 | 
			
		||||
		"rndstr": "1.0.0",
 | 
			
		||||
		"rollup": "3.9.0",
 | 
			
		||||
		"rollup": "3.9.1",
 | 
			
		||||
		"s-age": "1.1.2",
 | 
			
		||||
		"sanitize-html": "^2.8.1",
 | 
			
		||||
		"sass": "1.57.1",
 | 
			
		||||
@@ -55,14 +56,14 @@
 | 
			
		||||
		"textarea-caret": "3.1.0",
 | 
			
		||||
		"three": "0.148.0",
 | 
			
		||||
		"throttle-debounce": "5.0.0",
 | 
			
		||||
		"tinycolor2": "1.5.1",
 | 
			
		||||
		"tinycolor2": "1.5.2",
 | 
			
		||||
		"tsc-alias": "1.8.2",
 | 
			
		||||
		"tsconfig-paths": "4.1.2",
 | 
			
		||||
		"twemoji-parser": "14.0.0",
 | 
			
		||||
		"typescript": "4.9.4",
 | 
			
		||||
		"uuid": "9.0.0",
 | 
			
		||||
		"vanilla-tilt": "1.8.0",
 | 
			
		||||
		"vite": "4.0.3",
 | 
			
		||||
		"vite": "4.0.4",
 | 
			
		||||
		"vue": "3.2.45",
 | 
			
		||||
		"vue-prism-editor": "2.0.0-alpha.2",
 | 
			
		||||
		"vuedraggable": "next"
 | 
			
		||||
@@ -72,7 +73,7 @@
 | 
			
		||||
		"@types/glob": "8.0.0",
 | 
			
		||||
		"@types/gulp": "4.0.10",
 | 
			
		||||
		"@types/gulp-rename": "2.0.1",
 | 
			
		||||
		"@types/katex": "0.14.0",
 | 
			
		||||
		"@types/katex": "0.16.0",
 | 
			
		||||
		"@types/matter-js": "0.18.2",
 | 
			
		||||
		"@types/punycode": "2.1.0",
 | 
			
		||||
		"@types/sanitize-html": "^2.8.0",
 | 
			
		||||
@@ -82,16 +83,16 @@
 | 
			
		||||
		"@types/uuid": "9.0.0",
 | 
			
		||||
		"@types/websocket": "1.0.5",
 | 
			
		||||
		"@types/ws": "8.5.4",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.47.1",
 | 
			
		||||
		"@typescript-eslint/parser": "5.47.1",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.48.0",
 | 
			
		||||
		"@typescript-eslint/parser": "5.48.0",
 | 
			
		||||
		"@vue/runtime-core": "3.2.45",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"cypress": "12.2.0",
 | 
			
		||||
		"cypress": "12.3.0",
 | 
			
		||||
		"eslint": "8.31.0",
 | 
			
		||||
		"eslint-plugin-import": "2.26.0",
 | 
			
		||||
		"eslint-plugin-vue": "9.8.0",
 | 
			
		||||
		"start-server-and-test": "1.15.2",
 | 
			
		||||
		"vue-eslint-parser": "^9.1.0",
 | 
			
		||||
		"vue-tsc": "^1.0.19"
 | 
			
		||||
		"vue-tsc": "^1.0.22"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,13 @@ import { del, get, set } from '@/scripts/idb-proxy';
 | 
			
		||||
import { apiUrl } from '@/config';
 | 
			
		||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
 | 
			
		||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
 | 
			
		||||
import { miLocalStorage } from './local-storage';
 | 
			
		||||
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
 | 
			
		||||
type Account = misskey.entities.MeDetailed;
 | 
			
		||||
 | 
			
		||||
const accountData = localStorage.getItem('account');
 | 
			
		||||
const accountData = miLocalStorage.getItem('account');
 | 
			
		||||
 | 
			
		||||
// TODO: 外部からはreadonlyに
 | 
			
		||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
 | 
			
		||||
@@ -21,7 +22,7 @@ export const iAmAdmin = $i != null && $i.isAdmin;
 | 
			
		||||
 | 
			
		||||
export async function signout() {
 | 
			
		||||
	waiting();
 | 
			
		||||
	localStorage.removeItem('account');
 | 
			
		||||
	miLocalStorage.removeItem('account');
 | 
			
		||||
 | 
			
		||||
	await removeAccount($i.id);
 | 
			
		||||
 | 
			
		||||
@@ -119,7 +120,7 @@ export function updateAccount(accountData) {
 | 
			
		||||
	for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
		$i[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
	localStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function refreshAccount() {
 | 
			
		||||
@@ -130,7 +131,7 @@ export async function login(token: Account['token'], redirect?: string) {
 | 
			
		||||
	waiting();
 | 
			
		||||
	if (_DEV_) console.log('logging as token ', token);
 | 
			
		||||
	const me = await fetchAccount(token);
 | 
			
		||||
	localStorage.setItem('account', JSON.stringify(me));
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify(me));
 | 
			
		||||
	document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う
 | 
			
		||||
	await addAccount(me.id, token);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="bcekxzvu _gap _panel">
 | 
			
		||||
<div class="bcekxzvu _margin _panel">
 | 
			
		||||
	<div class="target">
 | 
			
		||||
		<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
 | 
			
		||||
			<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true" :disable-link="true"/>
 | 
			
		||||
@@ -8,7 +8,7 @@
 | 
			
		||||
				<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<MkKeyValue class="_formBlock">
 | 
			
		||||
		<MkKeyValue>
 | 
			
		||||
			<template #key>{{ i18n.ts.registeredDate }}</template>
 | 
			
		||||
			<template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
 | 
			
		||||
		</MkKeyValue>
 | 
			
		||||
@@ -37,7 +37,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkSwitch from '@/components/form/switch.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import MkKeyValue from '@/components/MkKeyValue.vue';
 | 
			
		||||
import { acct, userPage } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 | 
			
		||||
<MkWindow ref="uiWindow" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<i class="ti ti-exclamation-circle" style="margin-right: 0.5em;"></i>
 | 
			
		||||
		<I18n :src="i18n.ts.reportAbuseOf" tag="span">
 | 
			
		||||
@@ -8,25 +8,27 @@
 | 
			
		||||
			</template>
 | 
			
		||||
		</I18n>
 | 
			
		||||
	</template>
 | 
			
		||||
	<div class="dpvffvvy _monolithic_">
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<MkTextarea v-model="comment">
 | 
			
		||||
				<template #label>{{ i18n.ts.details }}</template>
 | 
			
		||||
				<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
 | 
			
		||||
			</MkTextarea>
 | 
			
		||||
	<MkSpacer :margin-min="20" :margin-max="28">
 | 
			
		||||
		<div class="dpvffvvy _gaps_m">
 | 
			
		||||
			<div class="">
 | 
			
		||||
				<MkTextarea v-model="comment">
 | 
			
		||||
					<template #label>{{ i18n.ts.details }}</template>
 | 
			
		||||
					<template #caption>{{ i18n.ts.fillAbuseReportDescription }}</template>
 | 
			
		||||
				</MkTextarea>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="">
 | 
			
		||||
				<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.ts.send }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</XWindow>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, shallowRef } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import MkTextarea from '@/components/form/textarea.vue';
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
@@ -40,7 +42,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const uiWindow = shallowRef<InstanceType<typeof XWindow>>();
 | 
			
		||||
const uiWindow = shallowRef<InstanceType<typeof MkWindow>>();
 | 
			
		||||
const comment = ref(props.initialComment || '');
 | 
			
		||||
 | 
			
		||||
function send() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										114
									
								
								packages/frontend/src/components/MkAsUi.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								packages/frontend/src/components/MkAsUi.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<div v-if="c.type === 'root'" :class="$style.root">
 | 
			
		||||
		<template v-for="child in c.children" :key="child">
 | 
			
		||||
			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
 | 
			
		||||
		</template>
 | 
			
		||||
	</div>
 | 
			
		||||
	<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
 | 
			
		||||
	<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, color: c.color ?? null }" :text="c.text"/>
 | 
			
		||||
	<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="c.onClick">{{ c.text }}</MkButton>
 | 
			
		||||
	<div v-else-if="c.type === 'buttons'" class="_buttons">
 | 
			
		||||
		<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkSwitch v-else-if="c.type === 'switch'" :model-value="valueForSwitch" @update:model-value="onSwitchUpdate">
 | 
			
		||||
		<template v-if="c.label" #label>{{ c.label }}</template>
 | 
			
		||||
		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 | 
			
		||||
	</MkSwitch>
 | 
			
		||||
	<MkTextarea v-else-if="c.type === 'textarea'" :model-value="c.default" @update:model-value="c.onInput">
 | 
			
		||||
		<template v-if="c.label" #label>{{ c.label }}</template>
 | 
			
		||||
		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 | 
			
		||||
	</MkTextarea>
 | 
			
		||||
	<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onInput">
 | 
			
		||||
		<template v-if="c.label" #label>{{ c.label }}</template>
 | 
			
		||||
		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :model-value="c.default" type="number" @update:model-value="c.onInput">
 | 
			
		||||
		<template v-if="c.label" #label>{{ c.label }}</template>
 | 
			
		||||
		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :model-value="c.default" @update:model-value="c.onChange">
 | 
			
		||||
		<template v-if="c.label" #label>{{ c.label }}</template>
 | 
			
		||||
		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 | 
			
		||||
		<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
 | 
			
		||||
	</MkSelect>
 | 
			
		||||
	<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" @click="openPostForm">{{ c.text }}</MkButton>
 | 
			
		||||
	<FormFolder v-else-if="c.type === 'folder'" :default-open="c.opened">
 | 
			
		||||
		<template #label>{{ c.title }}</template>
 | 
			
		||||
		<template v-for="child in c.children" :key="child">
 | 
			
		||||
			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
 | 
			
		||||
		</template>
 | 
			
		||||
	</FormFolder>
 | 
			
		||||
	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace', [$style.containerCenter]: c.align === 'center' }]" :style="{ backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
 | 
			
		||||
		<template v-for="child in c.children" :key="child">
 | 
			
		||||
			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
 | 
			
		||||
		</template>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, Ref } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import { AsUiComponent } from '@/scripts/aiscript/ui';
 | 
			
		||||
import FormFolder from '@/components/form/folder.vue';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	component: AsUiComponent;
 | 
			
		||||
	components: Ref<AsUiComponent>[];
 | 
			
		||||
	size: 'small' | 'medium' | 'large';
 | 
			
		||||
}>(), {
 | 
			
		||||
	size: 'medium',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const c = props.component;
 | 
			
		||||
 | 
			
		||||
function g(id) {
 | 
			
		||||
	return props.components.find(x => x.value.id === id).value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let valueForSwitch = $ref(c.default ?? false);
 | 
			
		||||
 | 
			
		||||
function onSwitchUpdate(v) {
 | 
			
		||||
	valueForSwitch = v;
 | 
			
		||||
	if (c.onChange) c.onChange(v);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openPostForm() {
 | 
			
		||||
	os.post({
 | 
			
		||||
		initialText: c.form.text,
 | 
			
		||||
		instant: true,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	gap: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	gap: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.containerCenter {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fontSerif {
 | 
			
		||||
	font-family: serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fontMonospace {
 | 
			
		||||
	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -46,6 +46,7 @@ import { defaultStore } from '@/store';
 | 
			
		||||
import { emojilist } from '@/scripts/emojilist';
 | 
			
		||||
import { instance } from '@/instance';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
 | 
			
		||||
type EmojiDef = {
 | 
			
		||||
	emoji: string;
 | 
			
		||||
@@ -208,7 +209,7 @@ function exec() {
 | 
			
		||||
		}
 | 
			
		||||
	} else if (props.type === 'hashtag') {
 | 
			
		||||
		if (!props.q || props.q === '') {
 | 
			
		||||
			hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
 | 
			
		||||
			hashtags.value = JSON.parse(miLocalStorage.getItem('hashtags') || '[]');
 | 
			
		||||
			fetching.value = false;
 | 
			
		||||
		} else {
 | 
			
		||||
			const cacheKey = `autocomplete:hashtag:${props.q}`;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
<button
 | 
			
		||||
	v-if="!link"
 | 
			
		||||
	ref="el" class="bghgjjyj _button"
 | 
			
		||||
	:class="{ inline, primary, gradate, danger, rounded, full, small }"
 | 
			
		||||
	:class="{ inline, primary, gradate, danger, rounded, full, small, large, asLike }"
 | 
			
		||||
	:type="type"
 | 
			
		||||
	@click="emit('click', $event)"
 | 
			
		||||
	@mousedown="onMousedown"
 | 
			
		||||
@@ -41,6 +41,8 @@ const props = defineProps<{
 | 
			
		||||
	danger?: boolean;
 | 
			
		||||
	full?: boolean;
 | 
			
		||||
	small?: boolean;
 | 
			
		||||
	large?: boolean;
 | 
			
		||||
	asLike?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
@@ -131,6 +133,11 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
		padding: 6px 12px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.large {
 | 
			
		||||
		font-size: 100%;
 | 
			
		||||
		padding: 8px 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.full {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
	}
 | 
			
		||||
@@ -153,6 +160,37 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.asLike {
 | 
			
		||||
		background: rgba(255, 86, 125, 0.07);
 | 
			
		||||
		color: #ff002f;
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):hover {
 | 
			
		||||
			background: rgba(255, 74, 116, 0.11);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):active {
 | 
			
		||||
			background: rgba(224, 57, 96, 0.125);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .ripples {
 | 
			
		||||
			::v-deep(div) {
 | 
			
		||||
				background: rgba(255, 60, 106, 0.15);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&.primary {
 | 
			
		||||
			background: rgb(241 97 132);
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):hover {
 | 
			
		||||
				background: rgb(241 92 128);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):active {
 | 
			
		||||
				background: rgb(241 92 128);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.gradate {
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
		color: var(--fgOnAccent) !important;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@
 | 
			
		||||
*/
 | 
			
		||||
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import { enUS } from 'date-fns/locale';
 | 
			
		||||
import gradient from 'chartjs-plugin-gradient';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
@@ -186,6 +185,10 @@ const render = () => {
 | 
			
		||||
					time: {
 | 
			
		||||
						stepSize: 1,
 | 
			
		||||
						unit: props.span === 'day' ? 'month' : 'day',
 | 
			
		||||
						displayFormats: {
 | 
			
		||||
							day: 'M/d',
 | 
			
		||||
							month: 'Y/M',
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
					},
 | 
			
		||||
@@ -194,11 +197,6 @@ const render = () => {
 | 
			
		||||
						maxRotation: 0,
 | 
			
		||||
						autoSkipPadding: 16,
 | 
			
		||||
					},
 | 
			
		||||
					adapters: {
 | 
			
		||||
						date: {
 | 
			
		||||
							locale: enUS,
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					min: getDate(props.limit).getTime(),
 | 
			
		||||
				},
 | 
			
		||||
				y: {
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ defineExpose({
 | 
			
		||||
 | 
			
		||||
			&.disabled {
 | 
			
		||||
				text-decoration: line-through;
 | 
			
		||||
				opacity: 0.6;
 | 
			
		||||
				opacity: 0.5;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> .box {
 | 
			
		||||
@@ -72,4 +72,11 @@ defineExpose({
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 500px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		font-size: 90%;
 | 
			
		||||
		gap: 6px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialogEl"
 | 
			
		||||
	:width="800"
 | 
			
		||||
	:height="500"
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</template>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
@@ -30,7 +30,7 @@ import { nextTick, onMounted } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import Cropper from 'cropperjs';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { $i } from '@/account';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
@@ -50,7 +50,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const imgUrl = getProxiedImageUrl(props.file.url);
 | 
			
		||||
let dialogEl = $shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
let dialogEl = $shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
let imgEl = $shallowRef<HTMLImageElement>();
 | 
			
		||||
let cropper: Cropper | null = null;
 | 
			
		||||
let loading = $ref(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,8 @@
 | 
			
		||||
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
 | 
			
		||||
import MkModal from '@/components/MkModal.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import MkSelect from '@/components/form/select.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
type Input = {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								packages/frontend/src/components/MkDonation.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								packages/frontend/src/components/MkDonation.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_panel _shadow" :class="$style.root">
 | 
			
		||||
	<!-- TODO: インスタンス運営者が任意のテキストとリンクを設定できるようにする -->
 | 
			
		||||
	<div :class="$style.icon">
 | 
			
		||||
		<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pig-money" width="40" height="40" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
 | 
			
		||||
			<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
 | 
			
		||||
			<path d="M15 11v.01"></path>
 | 
			
		||||
			<path d="M5.173 8.378a3 3 0 1 1 4.656 -1.377"></path>
 | 
			
		||||
			<path d="M16 4v3.803a6.019 6.019 0 0 1 2.658 3.197h1.341a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-1.342c-.336 .95 -.907 1.8 -1.658 2.473v2.027a1.5 1.5 0 0 1 -3 0v-.583a6.04 6.04 0 0 1 -1 .083h-4a6.04 6.04 0 0 1 -1 -.083v.583a1.5 1.5 0 0 1 -3 0v-2l.001 -.027a6 6 0 0 1 3.999 -10.473h2.5l4.5 -3h.001z"></path>
 | 
			
		||||
		</svg>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.main">
 | 
			
		||||
		<div :class="$style.title">{{ i18n.ts.didYouLikeMisskey }}</div>
 | 
			
		||||
		<div :class="$style.text">
 | 
			
		||||
			<I18n :src="i18n.ts.pleaseDonate" tag="span">
 | 
			
		||||
				<template #host>
 | 
			
		||||
					{{ $instance.name ?? host }}
 | 
			
		||||
				</template>
 | 
			
		||||
			</I18n>
 | 
			
		||||
			<div style="margin-top: 0.2em;">
 | 
			
		||||
				<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="_buttons">
 | 
			
		||||
			<MkButton @click="close">{{ i18n.ts.remindMeLater }}</MkButton>
 | 
			
		||||
			<MkButton @click="neverShow">{{ i18n.ts.neverShow }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, shallowRef } from 'vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkLink from '@/components/MkLink.vue';
 | 
			
		||||
import { host } from '@/config';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const zIndex = os.claimZIndex('low');
 | 
			
		||||
 | 
			
		||||
function close() {
 | 
			
		||||
	miLocalStorage.setItem('latestDonationInfoShownAt', Date.now().toString());
 | 
			
		||||
	emit('closed');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function neverShow() {
 | 
			
		||||
	miLocalStorage.setItem('neverShowDonationInfo', 'true')
 | 
			
		||||
	close();
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	position: fixed;
 | 
			
		||||
	z-index: v-bind(zIndex);
 | 
			
		||||
	bottom: var(--margin);
 | 
			
		||||
	left: 0;
 | 
			
		||||
	right: 0;
 | 
			
		||||
	margin: auto;
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	width: calc(100% - (var(--margin) * 2));
 | 
			
		||||
	max-width: 500px;
 | 
			
		||||
	display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	padding-top: 25px;
 | 
			
		||||
	width: 100px;
 | 
			
		||||
	color: var(--accent);
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 500px) {
 | 
			
		||||
	.icon {
 | 
			
		||||
		width: 80px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@media (max-width: 450px) {
 | 
			
		||||
	.icon {
 | 
			
		||||
		width: 70px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
	padding: 25px 25px 25px 0;
 | 
			
		||||
	flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.close {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 8px;
 | 
			
		||||
	right: 8px;
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.title {
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	margin: 0.7em 0 1em 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="800"
 | 
			
		||||
	:height="500"
 | 
			
		||||
@@ -15,14 +15,14 @@
 | 
			
		||||
		<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
 | 
			
		||||
	</template>
 | 
			
		||||
	<XDrive :multiple="multiple" :select="type" @change-selection="onChangeSelection" @selected="ok()"/>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, shallowRef } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XDrive from '@/components/MkDrive.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import number from '@/filters/number';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
 | 
			
		||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XWindow
 | 
			
		||||
<MkWindow
 | 
			
		||||
	ref="window"
 | 
			
		||||
	:initial-width="800"
 | 
			
		||||
	:initial-height="500"
 | 
			
		||||
@@ -10,14 +10,14 @@
 | 
			
		||||
		{{ i18n.ts.drive }}
 | 
			
		||||
	</template>
 | 
			
		||||
	<XDrive :initial-folder="initialFolder"/>
 | 
			
		||||
</XWindow>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XDrive from '@/components/MkDrive.vue';
 | 
			
		||||
import XWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
 | 
			
		||||
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
 | 
			
		||||
	<input ref="search" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
 | 
			
		||||
	<div ref="emojis" class="emojis">
 | 
			
		||||
		<section class="result">
 | 
			
		||||
@@ -94,6 +94,7 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
	asReactionPicker?: boolean;
 | 
			
		||||
	maxHeight?: number;
 | 
			
		||||
	asDrawer?: boolean;
 | 
			
		||||
	asWindow?: boolean;
 | 
			
		||||
}>(), {
 | 
			
		||||
	showPinned: true,
 | 
			
		||||
});
 | 
			
		||||
@@ -440,6 +441,28 @@ defineExpose({
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.asWindow {
 | 
			
		||||
		width: 100% !important;
 | 
			
		||||
		height: 100% !important;
 | 
			
		||||
 | 
			
		||||
		> .emojis {
 | 
			
		||||
			::v-deep(section) {
 | 
			
		||||
				> .body {
 | 
			
		||||
					display: grid;
 | 
			
		||||
					grid-template-columns: var(--columns);
 | 
			
		||||
					font-size: 30px;
 | 
			
		||||
 | 
			
		||||
					> .item {
 | 
			
		||||
						aspect-ratio: 1 / 1;
 | 
			
		||||
						width: auto;
 | 
			
		||||
						height: auto;
 | 
			
		||||
						min-width: 0;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .search {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		padding: 12px;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkWindow ref="window"
 | 
			
		||||
	:initial-width="null"
 | 
			
		||||
	:initial-height="null"
 | 
			
		||||
	:can-resize="false"
 | 
			
		||||
	:initial-width="300"
 | 
			
		||||
	:initial-height="290"
 | 
			
		||||
	:can-resize="true"
 | 
			
		||||
	:mini="true"
 | 
			
		||||
	:front="true"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
 | 
			
		||||
	<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" as-window @chosen="chosen" :class="$style.picker"/>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -34,147 +34,8 @@ function chosen(emoji: any) {
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.omfetrab {
 | 
			
		||||
	$pad: 8px;
 | 
			
		||||
	--eachSize: 40px;
 | 
			
		||||
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	contain: content;
 | 
			
		||||
 | 
			
		||||
	&.big {
 | 
			
		||||
		--eachSize: 44px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.w1 {
 | 
			
		||||
		width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.w2 {
 | 
			
		||||
		width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.w3 {
 | 
			
		||||
		width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.h1 {
 | 
			
		||||
		--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.h2 {
 | 
			
		||||
		--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.h3 {
 | 
			
		||||
		--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .search {
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		padding: 12px;
 | 
			
		||||
		box-sizing: border-box;
 | 
			
		||||
		font-size: 1em;
 | 
			
		||||
		outline: none;
 | 
			
		||||
		border: none;
 | 
			
		||||
		background: transparent;
 | 
			
		||||
		color: var(--fg);
 | 
			
		||||
 | 
			
		||||
		&:not(.filled) {
 | 
			
		||||
			order: 1;
 | 
			
		||||
			z-index: 2;
 | 
			
		||||
			box-shadow: 0px -1px 0 0px var(--divider);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .emojis {
 | 
			
		||||
		height: var(--height);
 | 
			
		||||
		overflow-y: auto;
 | 
			
		||||
		overflow-x: hidden;
 | 
			
		||||
 | 
			
		||||
		scrollbar-width: none;
 | 
			
		||||
 | 
			
		||||
		&::-webkit-scrollbar {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> .index {
 | 
			
		||||
			min-height: var(--height);
 | 
			
		||||
			position: relative;
 | 
			
		||||
			border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
				
 | 
			
		||||
			> .arrow {
 | 
			
		||||
				position: absolute;
 | 
			
		||||
				bottom: 0;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				padding: 16px 0;
 | 
			
		||||
				text-align: center;
 | 
			
		||||
				opacity: 0.5;
 | 
			
		||||
				pointer-events: none;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		section {
 | 
			
		||||
			> header {
 | 
			
		||||
				position: sticky;
 | 
			
		||||
				top: 0;
 | 
			
		||||
				left: 0;
 | 
			
		||||
				z-index: 1;
 | 
			
		||||
				padding: 8px;
 | 
			
		||||
				font-size: 12px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> div {
 | 
			
		||||
				padding: $pad;
 | 
			
		||||
 | 
			
		||||
				> button {
 | 
			
		||||
					position: relative;
 | 
			
		||||
					padding: 0;
 | 
			
		||||
					width: var(--eachSize);
 | 
			
		||||
					height: var(--eachSize);
 | 
			
		||||
					border-radius: 4px;
 | 
			
		||||
 | 
			
		||||
					&:focus-visible {
 | 
			
		||||
						outline: solid 2px var(--focus);
 | 
			
		||||
						z-index: 1;
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&:hover {
 | 
			
		||||
						background: rgba(0, 0, 0, 0.05);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					&:active {
 | 
			
		||||
						background: var(--accent);
 | 
			
		||||
						box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					> * {
 | 
			
		||||
						font-size: 24px;
 | 
			
		||||
						height: 1.25em;
 | 
			
		||||
						vertical-align: -.25em;
 | 
			
		||||
						pointer-events: none;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.result {
 | 
			
		||||
				border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
 | 
			
		||||
				&:empty {
 | 
			
		||||
					display: none;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.unicode {
 | 
			
		||||
				min-height: 384px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&.custom {
 | 
			
		||||
				min-height: 64px;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.picker {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="400"
 | 
			
		||||
	:height="450"
 | 
			
		||||
@@ -16,14 +16,14 @@
 | 
			
		||||
			<template #label>{{ i18n.ts.caption }}</template>
 | 
			
		||||
		</MkTextarea>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkTextarea from '@/components/form/textarea.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
@@ -37,7 +37,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
 | 
			
		||||
let caption = $ref(props.default);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -54,8 +54,6 @@ const props = defineProps<{
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.urempief {
 | 
			
		||||
	margin-top: var(--margin);
 | 
			
		||||
 | 
			
		||||
	&.list {
 | 
			
		||||
		> .file {
 | 
			
		||||
			display: flex;
 | 
			
		||||
@@ -89,7 +87,6 @@ const props = defineProps<{
 | 
			
		||||
		display: grid;
 | 
			
		||||
		grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
 | 
			
		||||
		grid-gap: 12px;
 | 
			
		||||
		margin: var(--margin) 0;
 | 
			
		||||
 | 
			
		||||
		> .file {
 | 
			
		||||
			position: relative;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										112
									
								
								packages/frontend/src/components/MkFlashPreview.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								packages/frontend/src/components/MkFlashPreview.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkA :to="`/play/${flash.id}`" class="vhpxefrk _panel" tabindex="-1">
 | 
			
		||||
	<article>
 | 
			
		||||
		<header>
 | 
			
		||||
			<h1 :title="flash.title">{{ flash.title }}</h1>
 | 
			
		||||
		</header>
 | 
			
		||||
		<p v-if="flash.summary" :title="flash.summary">{{ flash.summary.length > 85 ? flash.summary.slice(0, 85) + '…' : flash.summary }}</p>
 | 
			
		||||
		<footer>
 | 
			
		||||
			<img class="icon" :src="flash.user.avatarUrl"/>
 | 
			
		||||
			<p>{{ userName(flash.user) }}</p>
 | 
			
		||||
		</footer>
 | 
			
		||||
	</article>
 | 
			
		||||
</MkA>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import { userName } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	//flash: misskey.entities.Flash;
 | 
			
		||||
	flash: any;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.vhpxefrk {
 | 
			
		||||
	display: block;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
		color: var(--accent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> article {
 | 
			
		||||
		padding: 16px;
 | 
			
		||||
 | 
			
		||||
		> header {
 | 
			
		||||
			margin-bottom: 8px;
 | 
			
		||||
 | 
			
		||||
			> h1 {
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				font-size: 1em;
 | 
			
		||||
				color: var(--urlPreviewTitle);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> p {
 | 
			
		||||
			margin: 0;
 | 
			
		||||
			color: var(--urlPreviewText);
 | 
			
		||||
			font-size: 0.8em;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		> footer {
 | 
			
		||||
			margin-top: 8px;
 | 
			
		||||
			height: 16px;
 | 
			
		||||
 | 
			
		||||
			> img {
 | 
			
		||||
				display: inline-block;
 | 
			
		||||
				width: 16px;
 | 
			
		||||
				height: 16px;
 | 
			
		||||
				margin-right: 4px;
 | 
			
		||||
				vertical-align: top;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> p {
 | 
			
		||||
				display: inline-block;
 | 
			
		||||
				margin: 0;
 | 
			
		||||
				color: var(--urlPreviewInfo);
 | 
			
		||||
				font-size: 0.8em;
 | 
			
		||||
				line-height: 16px;
 | 
			
		||||
				vertical-align: top;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (max-width: 700px) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (max-width: 550px) {
 | 
			
		||||
		font-size: 12px;
 | 
			
		||||
 | 
			
		||||
		> article {
 | 
			
		||||
			padding: 12px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (max-width: 500px) {
 | 
			
		||||
		font-size: 10px;
 | 
			
		||||
		
 | 
			
		||||
		> article {
 | 
			
		||||
			padding: 8px;
 | 
			
		||||
 | 
			
		||||
			> header {
 | 
			
		||||
				margin-bottom: 4px;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			> footer {
 | 
			
		||||
				margin-top: 4px;
 | 
			
		||||
 | 
			
		||||
				> img {
 | 
			
		||||
					width: 12px;
 | 
			
		||||
					height: 12px;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -25,8 +25,9 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
 | 
			
		||||
const localStoragePrefix = 'ui:folder:';
 | 
			
		||||
const miLocalStoragePrefix = 'ui:folder:' as const;
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
@@ -44,13 +45,13 @@ export default defineComponent({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			bg: null,
 | 
			
		||||
			showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
 | 
			
		||||
			showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		showBody() {
 | 
			
		||||
			if (this.persistKey) {
 | 
			
		||||
				localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
 | 
			
		||||
				miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow ref="dialog"
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="370"
 | 
			
		||||
	:height="400"
 | 
			
		||||
	@close="dialog.close()"
 | 
			
		||||
@@ -8,18 +9,18 @@
 | 
			
		||||
	<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>
 | 
			
		||||
		<div class="main _gaps_m">
 | 
			
		||||
			<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required>
 | 
			
		||||
				<template #label>{{ i18n.ts.username }}</template>
 | 
			
		||||
				<template #prefix>@</template>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
 | 
			
		||||
			<MkInput v-model="email" class="_formBlock" type="email" :spellcheck="false" required>
 | 
			
		||||
			<MkInput v-model="email" type="email" :spellcheck="false" required>
 | 
			
		||||
				<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.ts.send }}</MkButton>
 | 
			
		||||
			<MkButton type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.ts.send }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="sub">
 | 
			
		||||
			<MkA to="/about" class="_link">{{ i18n.ts._forgotPassword.ifNoEmail }}</MkA>
 | 
			
		||||
@@ -28,14 +29,14 @@
 | 
			
		||||
	<div v-else class="bafecedb">
 | 
			
		||||
		{{ i18n.ts._forgotPassword.contactAdmin }}
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { instance } from '@/instance';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
@@ -45,7 +46,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let dialog: InstanceType<typeof XModalWindow> = $ref();
 | 
			
		||||
let dialog: InstanceType<typeof MkModalWindow> = $ref();
 | 
			
		||||
 | 
			
		||||
let username = $ref('');
 | 
			
		||||
let email = $ref('');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="450"
 | 
			
		||||
	:can-close="false"
 | 
			
		||||
@@ -15,66 +15,66 @@
 | 
			
		||||
	</template>
 | 
			
		||||
 | 
			
		||||
	<MkSpacer :margin-min="20" :margin-max="32">
 | 
			
		||||
		<div class="xkpnjxcv _formRoot">
 | 
			
		||||
		<div class="xkpnjxcv _gaps_m">
 | 
			
		||||
			<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
 | 
			
		||||
				<FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1" class="_formBlock">
 | 
			
		||||
				<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 | 
			
		||||
				</FormInput>
 | 
			
		||||
				<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text" class="_formBlock">
 | 
			
		||||
				</MkInput>
 | 
			
		||||
				<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 | 
			
		||||
				</FormInput>
 | 
			
		||||
				<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]" class="_formBlock">
 | 
			
		||||
				</MkInput>
 | 
			
		||||
				<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 | 
			
		||||
				</FormTextarea>
 | 
			
		||||
				<FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]" class="_formBlock">
 | 
			
		||||
				</MkTextarea>
 | 
			
		||||
				<MkSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
 | 
			
		||||
					<span v-text="form[item].label || item"></span>
 | 
			
		||||
					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 | 
			
		||||
				</FormSwitch>
 | 
			
		||||
				<FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]" class="_formBlock">
 | 
			
		||||
				</MkSwitch>
 | 
			
		||||
				<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
 | 
			
		||||
				</FormSelect>
 | 
			
		||||
				<FormRadios v-else-if="form[item].type === 'radio'" v-model="values[item]" class="_formBlock">
 | 
			
		||||
				</MkSelect>
 | 
			
		||||
				<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
 | 
			
		||||
				</FormRadios>
 | 
			
		||||
				<FormRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter" class="_formBlock">
 | 
			
		||||
				</MkRadios>
 | 
			
		||||
				<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :text-converter="form[item].textConverter">
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 | 
			
		||||
				</FormRange>
 | 
			
		||||
				<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)">
 | 
			
		||||
				</MkRange>
 | 
			
		||||
				<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
 | 
			
		||||
					<span v-text="form[item].content || item"></span>
 | 
			
		||||
				</MkButton>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import FormInput from './form/input.vue';
 | 
			
		||||
import FormTextarea from './form/textarea.vue';
 | 
			
		||||
import FormSwitch from './form/switch.vue';
 | 
			
		||||
import FormSelect from './form/select.vue';
 | 
			
		||||
import FormRange from './form/range.vue';
 | 
			
		||||
import MkInput from './MkInput.vue';
 | 
			
		||||
import MkTextarea from './MkTextarea.vue';
 | 
			
		||||
import MkSwitch from './MkSwitch.vue';
 | 
			
		||||
import MkSelect from './MkSelect.vue';
 | 
			
		||||
import MkRange from './MkRange.vue';
 | 
			
		||||
import MkButton from './MkButton.vue';
 | 
			
		||||
import FormRadios from './form/radios.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkRadios from './MkRadios.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
		XModalWindow,
 | 
			
		||||
		FormInput,
 | 
			
		||||
		FormTextarea,
 | 
			
		||||
		FormSwitch,
 | 
			
		||||
		FormSelect,
 | 
			
		||||
		FormRange,
 | 
			
		||||
		MkModalWindow,
 | 
			
		||||
		MkInput,
 | 
			
		||||
		MkTextarea,
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
		MkSelect,
 | 
			
		||||
		MkRange,
 | 
			
		||||
		MkButton,
 | 
			
		||||
		FormRadios,
 | 
			
		||||
		MkRadios,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import { enUS } from 'date-fns/locale';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
@@ -149,7 +148,9 @@ async function renderChart() {
 | 
			
		||||
						round: 'week',
 | 
			
		||||
						isoWeekday: 0,
 | 
			
		||||
						displayFormats: {
 | 
			
		||||
							week: 'MMM dd',
 | 
			
		||||
							day: 'M/d',
 | 
			
		||||
							month: 'Y/M',
 | 
			
		||||
							week: 'M/d',
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
					grid: {
 | 
			
		||||
 
 | 
			
		||||
@@ -78,8 +78,8 @@ const inputEl = shallowRef<HTMLElement>();
 | 
			
		||||
const prefixEl = shallowRef<HTMLElement>();
 | 
			
		||||
const suffixEl = shallowRef<HTMLElement>();
 | 
			
		||||
const height =
 | 
			
		||||
	props.small ? 35 :
 | 
			
		||||
	props.large ? 39 :
 | 
			
		||||
	props.small ? 34 :
 | 
			
		||||
	props.large ? 40 :
 | 
			
		||||
	37;
 | 
			
		||||
 | 
			
		||||
const focus = () => inputEl.value.focus();
 | 
			
		||||
@@ -78,7 +78,7 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import MkSelect from '@/components/form/select.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import MkChart from '@/components/MkChart.vue';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ const menu = defaultStore.state.menu;
 | 
			
		||||
 | 
			
		||||
const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
 | 
			
		||||
	type: def.to ? 'link' : 'button',
 | 
			
		||||
	text: i18n.ts[def.title],
 | 
			
		||||
	text: def.title,
 | 
			
		||||
	icon: def.icon,
 | 
			
		||||
	to: def.to,
 | 
			
		||||
	action: def.action,
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
				<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
 | 
			
		||||
			</button>
 | 
			
		||||
			<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 | 
			
		||||
				<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch>
 | 
			
		||||
				<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
 | 
			
		||||
			</span>
 | 
			
		||||
			<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
 | 
			
		||||
				<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
 | 
			
		||||
@@ -58,7 +58,7 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
 | 
			
		||||
import { focusPrev, focusNext } from '@/scripts/focus';
 | 
			
		||||
import FormSwitch from '@/components/form/switch.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction } from '@/types/menu';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
@@ -217,6 +217,7 @@ onBeforeUnmount(() => {
 | 
			
		||||
			content: "";
 | 
			
		||||
			display: block;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			z-index: -1;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			right: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -267,9 +267,8 @@ defineExpose({
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .content {
 | 
			
		||||
		transform-style: preserve-3d;
 | 
			
		||||
    transform: perspective(50cm) translateZ(0px) translateY(0px) rotateX(0deg);
 | 
			
		||||
		transition: opacity 0.4s cubic-bezier(.5,-0.5,.75,1), transform 0.4s cubic-bezier(.5,-0.5,.75,1) !important;
 | 
			
		||||
    transform: translateY(0px);
 | 
			
		||||
		transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
.send-enter-from, .send-leave-to {
 | 
			
		||||
@@ -280,8 +279,7 @@ defineExpose({
 | 
			
		||||
	> .content {
 | 
			
		||||
		pointer-events: none;
 | 
			
		||||
		opacity: 0;
 | 
			
		||||
		transform-style: preserve-3d;
 | 
			
		||||
		transform: perspective(50cm) translateZ(-300px) translateY(-200px) rotateX(40deg);
 | 
			
		||||
		transform: translateY(-300px);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -383,7 +381,6 @@ defineExpose({
 | 
			
		||||
			mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			display: flex;
 | 
			
		||||
			container-type: inline-size;
 | 
			
		||||
 | 
			
		||||
			@media (max-width: 500px) {
 | 
			
		||||
				padding: 16px;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
 | 
			
		||||
	<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
 | 
			
		||||
	<div ref="rootEl" class="hrmcaedk" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
 | 
			
		||||
		<div class="header" @contextmenu="onContextmenu">
 | 
			
		||||
			<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="ti ti-arrow-left"></i></button>
 | 
			
		||||
			<span v-else style="display: inline-block; width: 20px"></span>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
 | 
			
		||||
	<div ref="rootEl" class="ebkgoccj _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
 | 
			
		||||
	<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
 | 
			
		||||
		<div ref="headerEl" class="header">
 | 
			
		||||
			<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
 | 
			
		||||
			<span class="title">
 | 
			
		||||
@@ -89,6 +89,7 @@ defineExpose({
 | 
			
		||||
	display: flex;
 | 
			
		||||
	flex-direction: column;
 | 
			
		||||
	contain: content;
 | 
			
		||||
	container-type: inline-size;
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
 | 
			
		||||
	--root-margin: 24px;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
	v-show="!isDeleted"
 | 
			
		||||
	ref="el"
 | 
			
		||||
	v-hotkey="keymap"
 | 
			
		||||
	class="lxwezrsl _block"
 | 
			
		||||
	class="lxwezrsl"
 | 
			
		||||
	:tabindex="!isDeleted ? '-1' : null"
 | 
			
		||||
	:class="{ renote: isRenote }"
 | 
			
		||||
>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="400"
 | 
			
		||||
	:height="450"
 | 
			
		||||
@@ -12,33 +12,33 @@
 | 
			
		||||
	<template #header>{{ i18n.ts.notificationSetting }}</template>
 | 
			
		||||
 | 
			
		||||
	<MkSpacer :margin-min="20" :margin-max="28">
 | 
			
		||||
		<div class="_formRoot">
 | 
			
		||||
		<div class="_gaps_m">
 | 
			
		||||
			<template v-if="showGlobalToggle">
 | 
			
		||||
				<MkSwitch v-model="useGlobalSetting" class="_formBlock">
 | 
			
		||||
				<MkSwitch v-model="useGlobalSetting">
 | 
			
		||||
					{{ i18n.ts.useGlobalSetting }}
 | 
			
		||||
					<template #caption>{{ i18n.ts.useGlobalSettingDesc }}</template>
 | 
			
		||||
				</MkSwitch>
 | 
			
		||||
			</template>
 | 
			
		||||
			<template v-if="!useGlobalSetting">
 | 
			
		||||
				<MkInfo class="_formBlock">{{ i18n.ts.notificationSettingDesc }}</MkInfo>
 | 
			
		||||
				<div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
 | 
			
		||||
				<MkInfo>{{ i18n.ts.notificationSettingDesc }}</MkInfo>
 | 
			
		||||
				<div class="_buttons">
 | 
			
		||||
					<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
 | 
			
		||||
					<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
 | 
			
		||||
				</div>
 | 
			
		||||
				<MkSwitch v-for="ntype in notificationTypes" class="_formBlock" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
 | 
			
		||||
				<MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype]">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
 | 
			
		||||
			</template>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import { notificationTypes } from 'misskey-js';
 | 
			
		||||
import MkSwitch from './form/switch.vue';
 | 
			
		||||
import MkSwitch from './MkSwitch.vue';
 | 
			
		||||
import MkInfo from './MkInfo.vue';
 | 
			
		||||
import MkButton from './MkButton.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
 | 
			
		||||
let includingTypes = $computed(() => props.includingTypes || []);
 | 
			
		||||
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
 | 
			
		||||
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
 | 
			
		||||
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj _block" tabindex="-1">
 | 
			
		||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
 | 
			
		||||
	<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
 | 
			
		||||
	<article>
 | 
			
		||||
		<header>
 | 
			
		||||
@@ -14,22 +14,15 @@
 | 
			
		||||
</MkA>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import { userName } from '@/filters/user';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	props: {
 | 
			
		||||
		page: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		userName,
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	page: misskey.entities.Page;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XWindow
 | 
			
		||||
<MkWindow
 | 
			
		||||
	ref="windowEl"
 | 
			
		||||
	:initial-width="500"
 | 
			
		||||
	:initial-height="500"
 | 
			
		||||
@@ -20,13 +20,13 @@
 | 
			
		||||
	<div class="yrolvcoq" :style="{ background: pageMetadata?.value?.bg }" style="container-type: inline-size;">
 | 
			
		||||
		<RouterView :router="router"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</XWindow>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ComputedRef, inject, provide } from 'vue';
 | 
			
		||||
import RouterView from '@/components/global/RouterView.vue';
 | 
			
		||||
import XWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import { popout as _popout } from '@/scripts/popout';
 | 
			
		||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
			
		||||
import { url } from '@/config';
 | 
			
		||||
@@ -47,7 +47,7 @@ defineEmits<{
 | 
			
		||||
const router = new Router(routes, props.initialPath);
 | 
			
		||||
 | 
			
		||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
 | 
			
		||||
let windowEl = $shallowRef<InstanceType<typeof XWindow>>();
 | 
			
		||||
let windowEl = $shallowRef<InstanceType<typeof MkWindow>>();
 | 
			
		||||
const history = $ref<{ path: string; key: any; }[]>([{
 | 
			
		||||
	path: router.getCurrentPath(),
 | 
			
		||||
	key: router.getCurrentKey(),
 | 
			
		||||
@@ -84,6 +84,7 @@ provideMetadataReceiver((info) => {
 | 
			
		||||
});
 | 
			
		||||
provide('shouldOmitHeaderTitle', true);
 | 
			
		||||
provide('shouldHeaderThin', true);
 | 
			
		||||
provide('forceSpacerMin', true);
 | 
			
		||||
 | 
			
		||||
const contextmenu = $computed(() => ([{
 | 
			
		||||
	icon: 'ti ti-player-eject',
 | 
			
		||||
@@ -136,5 +137,7 @@ defineExpose({
 | 
			
		||||
.yrolvcoq {
 | 
			
		||||
	min-height: 100%;
 | 
			
		||||
	background: var(--bg);
 | 
			
		||||
 | 
			
		||||
	--margin: var(--marginHalf);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,14 +14,14 @@
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div v-else ref="rootEl">
 | 
			
		||||
		<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
 | 
			
		||||
		<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
 | 
			
		||||
			<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
 | 
			
		||||
				{{ i18n.ts.loadMore }}
 | 
			
		||||
			</MkButton>
 | 
			
		||||
			<MkLoading v-else class="loading"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<slot :items="items"></slot>
 | 
			
		||||
		<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
 | 
			
		||||
		<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _margin">
 | 
			
		||||
			<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
 | 
			
		||||
				{{ i18n.ts.loadMore }}
 | 
			
		||||
			</MkButton>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,18 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
 | 
			
		||||
	<span class="text" :class="{ up }">+1</span>
 | 
			
		||||
	<span class="text" :class="{ up }">
 | 
			
		||||
		<XReactionIcon class="icon" :reaction="reaction"/>
 | 
			
		||||
	</span>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	reaction: string;
 | 
			
		||||
	x: number;
 | 
			
		||||
	y: number;
 | 
			
		||||
}>(), {
 | 
			
		||||
@@ -20,6 +24,7 @@ const emit = defineEmits<{
 | 
			
		||||
 | 
			
		||||
let up = $ref(false);
 | 
			
		||||
const zIndex = os.claimZIndex('veryLow');
 | 
			
		||||
const angle = (90 - (Math.random() * 180)) + 'deg';
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	window.setTimeout(() => {
 | 
			
		||||
@@ -55,10 +60,11 @@ onMounted(() => {
 | 
			
		||||
			font-weight: bold;
 | 
			
		||||
			transform: translateY(-30px);
 | 
			
		||||
			transition: transform 1s cubic-bezier(0,.5,0,1), opacity 1s cubic-bezier(.5,0,1,.5);
 | 
			
		||||
			will-change: opacity, transform;
 | 
			
		||||
 | 
			
		||||
			&.up {
 | 
			
		||||
				opacity: 0;
 | 
			
		||||
				transform: translateY(-50px);
 | 
			
		||||
				transform: translateY(-50px) rotateZ(v-bind(angle));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,9 +49,9 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watch } from 'vue';
 | 
			
		||||
import MkInput from './form/input.vue';
 | 
			
		||||
import MkSelect from './form/select.vue';
 | 
			
		||||
import MkSwitch from './form/switch.vue';
 | 
			
		||||
import MkInput from './MkInput.vue';
 | 
			
		||||
import MkSelect from './MkSelect.vue';
 | 
			
		||||
import MkSwitch from './MkSwitch.vue';
 | 
			
		||||
import MkButton from './MkButton.vue';
 | 
			
		||||
import { formatDateTimeString } from '@/scripts/format-time-string';
 | 
			
		||||
import { addTime } from '@/scripts/time';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @closed="emit('closed')">
 | 
			
		||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'high'" :src="src" :transparent-bg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
 | 
			
		||||
	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :as-drawer="type === 'drawer'" class="sfhdhdhq" :class="{ drawer: type === 'drawer' }" @close="modal.close()"/>
 | 
			
		||||
</MkModal>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -20,6 +20,7 @@ defineProps<{
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
	(ev: 'closing'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
 | 
			
		||||
 
 | 
			
		||||
@@ -98,6 +98,7 @@ import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account'
 | 
			
		||||
import { uploadFile } from '@/scripts/upload';
 | 
			
		||||
import { deepClone } from '@/scripts/clone';
 | 
			
		||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage';
 | 
			
		||||
 | 
			
		||||
const modal = inject('modal');
 | 
			
		||||
 | 
			
		||||
@@ -156,7 +157,7 @@ let autocomplete = $ref(null);
 | 
			
		||||
let draghover = $ref(false);
 | 
			
		||||
let quoteId = $ref(null);
 | 
			
		||||
let hasNotSpecifiedMentions = $ref(false);
 | 
			
		||||
let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
 | 
			
		||||
let recentHashtags = $ref(JSON.parse(miLocalStorage.getItem('hashtags') || '[]'));
 | 
			
		||||
let imeText = $ref('');
 | 
			
		||||
 | 
			
		||||
const typing = throttle(3000, () => {
 | 
			
		||||
@@ -543,7 +544,7 @@ function onDrop(ev): void {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveDraft() {
 | 
			
		||||
	const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
 | 
			
		||||
	const draftData = JSON.parse(miLocalStorage.getItem('drafts') || '{}');
 | 
			
		||||
 | 
			
		||||
	draftData[draftKey] = {
 | 
			
		||||
		updatedAt: new Date(),
 | 
			
		||||
@@ -558,15 +559,15 @@ function saveDraft() {
 | 
			
		||||
		},
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	localStorage.setItem('drafts', JSON.stringify(draftData));
 | 
			
		||||
	miLocalStorage.setItem('drafts', JSON.stringify(draftData));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function deleteDraft() {
 | 
			
		||||
	const draftData = JSON.parse(localStorage.getItem('drafts') ?? '{}');
 | 
			
		||||
	const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}');
 | 
			
		||||
 | 
			
		||||
	delete draftData[draftKey];
 | 
			
		||||
 | 
			
		||||
	localStorage.setItem('drafts', JSON.stringify(draftData));
 | 
			
		||||
	miLocalStorage.setItem('drafts', JSON.stringify(draftData));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function post(ev?: MouseEvent) {
 | 
			
		||||
@@ -622,8 +623,8 @@ async function post(ev?: MouseEvent) {
 | 
			
		||||
			emit('posted');
 | 
			
		||||
			if (postData.text && postData.text !== '') {
 | 
			
		||||
				const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
 | 
			
		||||
				const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
 | 
			
		||||
				localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
 | 
			
		||||
				const history = JSON.parse(miLocalStorage.getItem('hashtags') || '[]') as string[];
 | 
			
		||||
				miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
 | 
			
		||||
			}
 | 
			
		||||
			posting = false;
 | 
			
		||||
			postAccount = null;
 | 
			
		||||
@@ -698,7 +699,7 @@ onMounted(() => {
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		// 書きかけの投稿を復元
 | 
			
		||||
		if (!props.instant && !props.mention && !props.specified) {
 | 
			
		||||
			const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
 | 
			
		||||
			const draft = JSON.parse(miLocalStorage.getItem('drafts') || '{}')[draftKey];
 | 
			
		||||
			if (draft) {
 | 
			
		||||
				text = draft.data.text;
 | 
			
		||||
				useCw = draft.data.useCw;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, h } from 'vue';
 | 
			
		||||
import MkRadio from './radio.vue';
 | 
			
		||||
import MkRadio from './MkRadio.vue';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -61,7 +61,7 @@ const anime = () => {
 | 
			
		||||
	const rect = buttonRef.value.getBoundingClientRect();
 | 
			
		||||
	const x = rect.left + (buttonRef.value.offsetWidth / 2);
 | 
			
		||||
	const y = rect.top + (buttonRef.value.offsetHeight / 2);
 | 
			
		||||
	os.popup(MkPlusOneEffect, { x, y }, {}, 'end');
 | 
			
		||||
	os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watch(() => props.count, (newCount, oldCount) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="jmgmzlwq _block"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
 | 
			
		||||
<div class="jmgmzlwq"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i>{{ i18n.ts.remoteUserCaution }}<a class="link" :href="href" rel="nofollow noopener" target="_blank">{{ i18n.ts.showOnRemote }}</a></div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
@@ -16,6 +16,8 @@ defineProps<{
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
	background: var(--infoWarnBg);
 | 
			
		||||
	color: var(--infoWarnFg);
 | 
			
		||||
	border-radius: var(--radius);
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
 | 
			
		||||
	> .link {
 | 
			
		||||
		margin-left: 4px;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import { enUS } from 'date-fns/locale';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
@@ -40,7 +39,7 @@ async function renderChart() {
 | 
			
		||||
	const wide = rootEl.offsetWidth > 600;
 | 
			
		||||
	const narrow = rootEl.offsetWidth < 400;
 | 
			
		||||
 | 
			
		||||
	const maxDays = wide ? 20 : narrow ? 7 : 14;
 | 
			
		||||
	const maxDays = wide ? 15 : narrow ? 5 : 10;
 | 
			
		||||
 | 
			
		||||
	const raw = await os.api('retention', { });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_card">
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
<div class="">
 | 
			
		||||
	<div class="">
 | 
			
		||||
		<MkInput v-model="text">
 | 
			
		||||
			<template #label>Text</template>
 | 
			
		||||
		</MkInput>
 | 
			
		||||
@@ -15,10 +15,10 @@
 | 
			
		||||
		<MkButton inline>This is</MkButton>
 | 
			
		||||
		<MkButton inline primary>the button</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content" style="pointer-events: none;">
 | 
			
		||||
	<div class="" style="pointer-events: none;">
 | 
			
		||||
		<Mfm :text="mfm"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_content">
 | 
			
		||||
	<div class="">
 | 
			
		||||
		<MkButton inline primary @click="openMenu">Open menu</MkButton>
 | 
			
		||||
		<MkButton inline primary @click="openDialog">Open dialog</MkButton>
 | 
			
		||||
		<MkButton inline primary @click="openForm">Open form</MkButton>
 | 
			
		||||
@@ -30,10 +30,10 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent } from 'vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import MkSwitch from '@/components/form/switch.vue';
 | 
			
		||||
import MkTextarea from '@/components/form/textarea.vue';
 | 
			
		||||
import MkRadio from '@/components/form/radio.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkRadio from '@/components/MkRadio.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import * as config from '@/config';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
		>
 | 
			
		||||
			<slot></slot>
 | 
			
		||||
		</select>
 | 
			
		||||
		<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down"></i></div>
 | 
			
		||||
		<div ref="suffixEl" class="suffix"><i class="ti ti-chevron-down" :class="[$style.chevron, { [$style.chevronOpening]: opening }]"></i></div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="caption"><slot name="caption"></slot></div>
 | 
			
		||||
 | 
			
		||||
@@ -56,6 +56,7 @@ const slots = useSlots();
 | 
			
		||||
const { modelValue, autofocus } = toRefs(props);
 | 
			
		||||
const v = ref(modelValue.value);
 | 
			
		||||
const focused = ref(false);
 | 
			
		||||
const opening = ref(false);
 | 
			
		||||
const changed = ref(false);
 | 
			
		||||
const invalid = ref(false);
 | 
			
		||||
const filled = computed(() => v.value !== '' && v.value != null);
 | 
			
		||||
@@ -64,8 +65,8 @@ const prefixEl = ref(null);
 | 
			
		||||
const suffixEl = ref(null);
 | 
			
		||||
const container = ref(null);
 | 
			
		||||
const height =
 | 
			
		||||
	props.small ? 35 :
 | 
			
		||||
	props.large ? 39 :
 | 
			
		||||
	props.small ? 34 :
 | 
			
		||||
	props.large ? 40 :
 | 
			
		||||
	37;
 | 
			
		||||
 | 
			
		||||
const focus = () => inputEl.value.focus();
 | 
			
		||||
@@ -119,6 +120,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
const onClick = (ev: MouseEvent) => {
 | 
			
		||||
	focused.value = true;
 | 
			
		||||
	opening.value = true;
 | 
			
		||||
 | 
			
		||||
	const menu = [];
 | 
			
		||||
	let options = slots.default!();
 | 
			
		||||
@@ -126,7 +128,7 @@ const onClick = (ev: MouseEvent) => {
 | 
			
		||||
	const pushOption = (option: VNode) => {
 | 
			
		||||
		menu.push({
 | 
			
		||||
			text: option.children,
 | 
			
		||||
			active: v.value === option.props.value,
 | 
			
		||||
			active: computed(() => v.value === option.props.value),
 | 
			
		||||
			action: () => {
 | 
			
		||||
				v.value = option.props.value;
 | 
			
		||||
			},
 | 
			
		||||
@@ -158,6 +160,9 @@ const onClick = (ev: MouseEvent) => {
 | 
			
		||||
 | 
			
		||||
	os.popupMenu(menu, container.value, {
 | 
			
		||||
		width: container.value.offsetWidth,
 | 
			
		||||
		onClosing: () => {
 | 
			
		||||
			opening.value = false;
 | 
			
		||||
		},
 | 
			
		||||
	}).then(() => {
 | 
			
		||||
		focused.value = false;
 | 
			
		||||
	});
 | 
			
		||||
@@ -277,3 +282,13 @@ const onClick = (ev: MouseEvent) => {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.chevron {
 | 
			
		||||
	transition: transform 0.5s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chevronOpening {
 | 
			
		||||
	transform: rotateX(180deg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
 | 
			
		||||
	<div class="auth _section _formRoot">
 | 
			
		||||
<form class="eppvobhk" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
 | 
			
		||||
	<div class="auth _gaps_m">
 | 
			
		||||
		<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
 | 
			
		||||
		<MkInfo v-if="message">
 | 
			
		||||
			{{ message }}
 | 
			
		||||
		</MkInfo>
 | 
			
		||||
		<div v-if="!totpLogin" class="normal-signin">
 | 
			
		||||
			<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
 | 
			
		||||
		<div v-if="!totpLogin" class="normal-signin _gaps_m">
 | 
			
		||||
			<MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autofocus required data-cy-signin-username @update:model-value="onUsernameChange">
 | 
			
		||||
				<template #prefix>@</template>
 | 
			
		||||
				<template #suffix>@{{ host }}</template>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
			<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
 | 
			
		||||
			<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
 | 
			
		||||
				<template #prefix><i class="ti ti-lock"></i></template>
 | 
			
		||||
				<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
 | 
			
		||||
			</MkInput>
 | 
			
		||||
			<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
 | 
			
		||||
			<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
 | 
			
		||||
			<div v-if="user && user.securityKeys" class="twofa-group tap-group">
 | 
			
		||||
@@ -40,10 +40,10 @@
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="social _section">
 | 
			
		||||
		<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
 | 
			
		||||
		<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
 | 
			
		||||
		<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _gap" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
 | 
			
		||||
	<div class="social">
 | 
			
		||||
		<a v-if="meta && meta.enableTwitterIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/twitter`"><i class="ti ti-brand-twitter" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
 | 
			
		||||
		<a v-if="meta && meta.enableGithubIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/github`"><i class="ti ti-brand-github" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
 | 
			
		||||
		<a v-if="meta && meta.enableDiscordIntegration" class="_borderButton _margin" :href="`${apiUrl}/signin/discord`"><i class="ti ti-brand-discord" style="margin-right: 4px;"></i>{{ $t('signinWith', { x: 'Discord' }) }}</a>
 | 
			
		||||
	</div>
 | 
			
		||||
</form>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -53,7 +53,7 @@ import { defineAsyncComponent } from 'vue';
 | 
			
		||||
import { toUnicode } from 'punycode/';
 | 
			
		||||
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkInfo from '@/components/MkInfo.vue';
 | 
			
		||||
import { apiUrl, host as configHost } from '@/config';
 | 
			
		||||
import { byteify, hexify } from '@/scripts/2fa';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="370"
 | 
			
		||||
	:height="400"
 | 
			
		||||
@@ -8,14 +8,16 @@
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ i18n.ts.login }}</template>
 | 
			
		||||
 | 
			
		||||
	<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
	<MkSpacer :margin-min="20" :margin-max="28">
 | 
			
		||||
		<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import MkSignin from '@/components/MkSignin.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
@@ -32,7 +34,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'cancelled'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
 | 
			
		||||
function onClose() {
 | 
			
		||||
	emit('cancelled');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
 | 
			
		||||
	<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
 | 
			
		||||
<form class="qlvuhzng _gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
 | 
			
		||||
	<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
 | 
			
		||||
		<template #label>{{ i18n.ts.invitationCode }}</template>
 | 
			
		||||
		<template #prefix><i class="ti ti-key"></i></template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
 | 
			
		||||
	<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
 | 
			
		||||
		<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
 | 
			
		||||
		<template #prefix>@</template>
 | 
			
		||||
		<template #suffix>@{{ host }}</template>
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
			<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
 | 
			
		||||
		</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
 | 
			
		||||
	<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
 | 
			
		||||
		<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
 | 
			
		||||
		<template #prefix><i class="ti ti-mail"></i></template>
 | 
			
		||||
		<template #caption>
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
			<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
 | 
			
		||||
		</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
 | 
			
		||||
	<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
 | 
			
		||||
		<template #label>{{ i18n.ts.password }}</template>
 | 
			
		||||
		<template #prefix><i class="ti ti-lock"></i></template>
 | 
			
		||||
		<template #caption>
 | 
			
		||||
@@ -42,7 +42,7 @@
 | 
			
		||||
			<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
 | 
			
		||||
		</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
 | 
			
		||||
	<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
 | 
			
		||||
		<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
 | 
			
		||||
		<template #prefix><i class="ti ti-lock"></i></template>
 | 
			
		||||
		<template #caption>
 | 
			
		||||
@@ -50,17 +50,17 @@
 | 
			
		||||
			<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
 | 
			
		||||
		</template>
 | 
			
		||||
	</MkInput>
 | 
			
		||||
	<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
 | 
			
		||||
	<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="tou">
 | 
			
		||||
		<I18n :src="i18n.ts.agreeTo">
 | 
			
		||||
			<template #0>
 | 
			
		||||
				<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a>
 | 
			
		||||
			</template>
 | 
			
		||||
		</I18n>
 | 
			
		||||
	</MkSwitch>
 | 
			
		||||
	<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
 | 
			
		||||
	<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 | 
			
		||||
	<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="_formBlock captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
 | 
			
		||||
	<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
 | 
			
		||||
	<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
 | 
			
		||||
	<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 | 
			
		||||
	<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
 | 
			
		||||
	<MkButton type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
 | 
			
		||||
</form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -69,8 +69,8 @@ import { } from 'vue';
 | 
			
		||||
import getPasswordStrength from 'syuilo-password-strength';
 | 
			
		||||
import { toUnicode } from 'punycode/';
 | 
			
		||||
import MkButton from './MkButton.vue';
 | 
			
		||||
import MkInput from './form/input.vue';
 | 
			
		||||
import MkSwitch from './form/switch.vue';
 | 
			
		||||
import MkInput from './MkInput.vue';
 | 
			
		||||
import MkSwitch from './MkSwitch.vue';
 | 
			
		||||
import MkCaptcha from '@/components/MkCaptcha.vue';
 | 
			
		||||
import * as config from '@/config';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="366"
 | 
			
		||||
	:height="500"
 | 
			
		||||
@@ -8,18 +8,16 @@
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ i18n.ts.signup }}</template>
 | 
			
		||||
 | 
			
		||||
	<div class="_monolithic_">
 | 
			
		||||
		<div class="_section">
 | 
			
		||||
			<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
	<MkSpacer :margin-min="20" :margin-max="28">
 | 
			
		||||
		<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import XSignup from '@/components/MkSignup.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
@@ -33,7 +31,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
 | 
			
		||||
function onSignup(res) {
 | 
			
		||||
	emit('done', res);
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ export default defineComponent({
 | 
			
		||||
				align-items: center;
 | 
			
		||||
				width: 100%;
 | 
			
		||||
				box-sizing: border-box;
 | 
			
		||||
				padding: 10px 16px 10px 8px;
 | 
			
		||||
				padding: 9px 16px 9px 8px;
 | 
			
		||||
				border-radius: 9px;
 | 
			
		||||
				font-size: 0.9em;
 | 
			
		||||
 | 
			
		||||
@@ -141,8 +141,8 @@ export default defineComponent({
 | 
			
		||||
						margin-right: 0;
 | 
			
		||||
						margin-bottom: 6px;
 | 
			
		||||
						font-size: 1.5em;
 | 
			
		||||
						width: 54px;
 | 
			
		||||
						height: 54px;
 | 
			
		||||
						width: 60px;
 | 
			
		||||
						height: 60px;
 | 
			
		||||
						aspect-ratio: 1;
 | 
			
		||||
						background: var(--panel);
 | 
			
		||||
						border-radius: 100%;
 | 
			
		||||
 
 | 
			
		||||
@@ -73,9 +73,9 @@ const toggle = () => {
 | 
			
		||||
		width: 32px;
 | 
			
		||||
		height: 23px;
 | 
			
		||||
		outline: none;
 | 
			
		||||
		background: var(--swutchOffBg);
 | 
			
		||||
		background: var(--switchOffBg);
 | 
			
		||||
		background-clip: content-box;
 | 
			
		||||
		border: solid 1px var(--swutchOffBg);
 | 
			
		||||
		border: solid 1px var(--switchOffBg);
 | 
			
		||||
		border-radius: 999px;
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
		transition: inherit;
 | 
			
		||||
@@ -87,7 +87,7 @@ const toggle = () => {
 | 
			
		||||
			left: 3px;
 | 
			
		||||
			width: 15px;
 | 
			
		||||
			height: 15px;
 | 
			
		||||
			background: var(--swutchOffFg);
 | 
			
		||||
			background: var(--switchOffFg);
 | 
			
		||||
			border-radius: 999px;
 | 
			
		||||
			transition: all 0.2s ease;
 | 
			
		||||
		}
 | 
			
		||||
@@ -131,12 +131,12 @@ const toggle = () => {
 | 
			
		||||
 | 
			
		||||
	&.checked {
 | 
			
		||||
		> .button {
 | 
			
		||||
			background-color: var(--swutchOnBg) !important;
 | 
			
		||||
			border-color: var(--swutchOnBg) !important;
 | 
			
		||||
			background-color: var(--switchOnBg) !important;
 | 
			
		||||
			border-color: var(--switchOnBg) !important;
 | 
			
		||||
 | 
			
		||||
			> .knob {
 | 
			
		||||
				left: 12px;
 | 
			
		||||
				background: var(--swutchOnFg);
 | 
			
		||||
				background: var(--switchOnFg);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialog"
 | 
			
		||||
	:width="400"
 | 
			
		||||
	:height="450"
 | 
			
		||||
@@ -11,31 +11,37 @@
 | 
			
		||||
	@ok="ok()"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ title || $ts.generateAccessToken }}</template>
 | 
			
		||||
	<div v-if="information" class="_section">
 | 
			
		||||
		<MkInfo warn>{{ information }}</MkInfo>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<MkInput v-model="name">
 | 
			
		||||
			<template #label>{{ $ts.name }}</template>
 | 
			
		||||
		</MkInput>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="_section">
 | 
			
		||||
		<div style="margin-bottom: 16px;"><b>{{ $ts.permission }}</b></div>
 | 
			
		||||
		<MkButton inline @click="disableAll">{{ $ts.disableAll }}</MkButton>
 | 
			
		||||
		<MkButton inline @click="enableAll">{{ $ts.enableAll }}</MkButton>
 | 
			
		||||
		<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
 | 
			
		||||
	<MkSpacer :margin-min="20" :margin-max="28">
 | 
			
		||||
		<div class="_gaps_m">
 | 
			
		||||
			<div v-if="information">
 | 
			
		||||
				<MkInfo warn>{{ information }}</MkInfo>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div>
 | 
			
		||||
				<MkInput v-model="name">
 | 
			
		||||
					<template #label>{{ $ts.name }}</template>
 | 
			
		||||
				</MkInput>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div><b>{{ $ts.permission }}</b></div>
 | 
			
		||||
			<div class="_buttons">
 | 
			
		||||
				<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
 | 
			
		||||
				<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</MkSwitch>
 | 
			
		||||
		</div>
 | 
			
		||||
	</MkSpacer>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import { permissions as kinds } from 'misskey-js';
 | 
			
		||||
import MkInput from './form/input.vue';
 | 
			
		||||
import MkSwitch from './form/switch.vue';
 | 
			
		||||
import MkInput from './MkInput.vue';
 | 
			
		||||
import MkSwitch from './MkSwitch.vue';
 | 
			
		||||
import MkButton from './MkButton.vue';
 | 
			
		||||
import MkInfo from './MkInfo.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	title?: string | null;
 | 
			
		||||
@@ -54,7 +60,7 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'done', result: { name: string | null, permissions: string[] }): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
 | 
			
		||||
let name = $ref(props.initialName);
 | 
			
		||||
let permissions = $ref({});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,13 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { shallowRef } from 'vue';
 | 
			
		||||
import { onMounted, shallowRef } from 'vue';
 | 
			
		||||
import MkModal from '@/components/MkModal.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import MkSparkle from '@/components/MkSparkle.vue';
 | 
			
		||||
import { version } from '@/config';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
import { confetti } from '@/scripts/confetti';
 | 
			
		||||
 | 
			
		||||
const modal = shallowRef<InstanceType<typeof MkModal>>();
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +24,12 @@ const whatIsNew = () => {
 | 
			
		||||
	modal.value.close();
 | 
			
		||||
	window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	confetti({
 | 
			
		||||
		duration: 1000 * 3,
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XModalWindow
 | 
			
		||||
<MkModalWindow
 | 
			
		||||
	ref="dialogEl"
 | 
			
		||||
	:with-ok-button="true"
 | 
			
		||||
	:ok-button-disabled="selected == null"
 | 
			
		||||
@@ -48,15 +48,15 @@
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</XModalWindow>
 | 
			
		||||
</MkModalWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { nextTick, onMounted } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import MkInput from '@/components/form/input.vue';
 | 
			
		||||
import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import FormSplit from '@/components/form/split.vue';
 | 
			
		||||
import XModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { defaultStore } from '@/store';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ export type DefaultStoredWidget = {
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { defineAsyncComponent, reactive, ref, computed } from 'vue';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import MkSelect from '@/components/form/select.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { widgets as widgetDefs } from '@/widgets';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<Transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
 | 
			
		||||
	<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
 | 
			
		||||
		<div class="body _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
 | 
			
		||||
		<div class="body _shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
 | 
			
		||||
			<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
 | 
			
		||||
				<span class="left">
 | 
			
		||||
					<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
 | 
			
		||||
@@ -61,8 +61,8 @@ function dragClear(fn) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	initialWidth?: number;
 | 
			
		||||
	initialHeight?: number | null;
 | 
			
		||||
	initialWidth: number;
 | 
			
		||||
	initialHeight: number | null;
 | 
			
		||||
	canResize?: boolean;
 | 
			
		||||
	closeButton?: boolean;
 | 
			
		||||
	mini?: boolean;
 | 
			
		||||
@@ -386,7 +386,7 @@ function onBrowserResize() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	if (props.initialWidth) applyTransformWidth(props.initialWidth);
 | 
			
		||||
	applyTransformWidth(props.initialWidth);
 | 
			
		||||
	if (props.initialHeight) applyTransformHeight(props.initialHeight);
 | 
			
		||||
 | 
			
		||||
	applyTransformTop((window.innerHeight / 2) - (rootEl.offsetHeight / 2));
 | 
			
		||||
@@ -489,6 +489,7 @@ defineExpose({
 | 
			
		||||
			flex: 1;
 | 
			
		||||
			overflow: auto;
 | 
			
		||||
			background: var(--panel);
 | 
			
		||||
			container-type: inline-size;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<XWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
 | 
			
		||||
<MkWindow :initial-width="640" :initial-height="402" :can-resize="true" :close-button="true">
 | 
			
		||||
	<template #header>
 | 
			
		||||
		<i class="icon ti ti-brand-youtube" style="margin-right: 0.5em;"></i>
 | 
			
		||||
		<span>{{ title ?? 'YouTube' }}</span>
 | 
			
		||||
@@ -14,11 +14,11 @@
 | 
			
		||||
		<MkLoading v-if="fetching"/>
 | 
			
		||||
		<MkError v-else-if="!player.url" @retry="ytFetch()"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</XWindow>
 | 
			
		||||
</MkWindow>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import XWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import MkWindow from '@/components/MkWindow.vue';
 | 
			
		||||
import { versatileLang } from '@/scripts/intl-const';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="dwzlatin" :class="{ opened }">
 | 
			
		||||
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
 | 
			
		||||
	<div class="header _button" @click="toggle">
 | 
			
		||||
		<span class="icon"><slot name="icon"></slot></span>
 | 
			
		||||
		<span class="text"><slot name="label"></slot></span>
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
			<i v-else class="ti ti-chevron-down icon"></i>
 | 
			
		||||
		</span>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="openedAtLeastOnce" class="body">
 | 
			
		||||
	<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }">
 | 
			
		||||
		<Transition
 | 
			
		||||
			:name="$store.state.animation ? 'folder-toggle' : ''"
 | 
			
		||||
			@enter="enter"
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { nextTick } from 'vue';
 | 
			
		||||
import { nextTick, onMounted } from 'vue';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	defaultOpen: boolean;
 | 
			
		||||
@@ -38,6 +38,17 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
	defaultOpen: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getBgColor = (el: HTMLElement) => {
 | 
			
		||||
	const style = window.getComputedStyle(el);
 | 
			
		||||
	if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
 | 
			
		||||
		return style.backgroundColor;
 | 
			
		||||
	} else {
 | 
			
		||||
		return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let rootEl = $ref<HTMLElement>();
 | 
			
		||||
let bgSame = $ref(false);
 | 
			
		||||
let opened = $ref(props.defaultOpen);
 | 
			
		||||
let openedAtLeastOnce = $ref(props.defaultOpen);
 | 
			
		||||
 | 
			
		||||
@@ -72,6 +83,13 @@ function toggle() {
 | 
			
		||||
		opened = !opened;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
	const parentBg = getBgColor(rootEl.parentElement);
 | 
			
		||||
	const myBg = computedStyle.getPropertyValue('--panel');
 | 
			
		||||
	bgSame = parentBg === myBg;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
@@ -142,6 +160,10 @@ function toggle() {
 | 
			
		||||
		background: var(--panel);
 | 
			
		||||
		border-radius: 0 0 6px 6px;
 | 
			
		||||
		container-type: inline-size;
 | 
			
		||||
 | 
			
		||||
		&.bgSame {
 | 
			
		||||
			background: var(--bg);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.opened {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,27 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="vrtktovh _formBlock">
 | 
			
		||||
<div class="vrtktovh" :class="{ first }">
 | 
			
		||||
	<div class="label"><slot name="label"></slot></div>
 | 
			
		||||
	<div class="main _formRoot">
 | 
			
		||||
	<div class="main">
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
defineProps<{
 | 
			
		||||
	first?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.vrtktovh {
 | 
			
		||||
	border-top: solid 0.5px var(--divider);
 | 
			
		||||
	border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
 | 
			
		||||
	& + .vrtktovh {
 | 
			
		||||
		border-top: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		border-top: none;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:last-child {
 | 
			
		||||
		border-bottom: none;
 | 
			
		||||
	}
 | 
			
		||||
	//border-bottom: solid 0.5px var(--divider);
 | 
			
		||||
 | 
			
		||||
	> .label {
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
		margin: 1.5em 0 16px 0;
 | 
			
		||||
		padding: 1.5em 0 0 0;
 | 
			
		||||
		margin: 0 0 16px 0;
 | 
			
		||||
 | 
			
		||||
		&:empty {
 | 
			
		||||
			display: none;
 | 
			
		||||
@@ -37,7 +29,15 @@
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	> .main {
 | 
			
		||||
		margin: 1.5em 0;
 | 
			
		||||
		margin: 1.5em 0 0 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.first {
 | 
			
		||||
		border-top: none;
 | 
			
		||||
 | 
			
		||||
		> .label {
 | 
			
		||||
			padding-top: 0;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="terlnhxf _formBlock">
 | 
			
		||||
<div class="terlnhxf">
 | 
			
		||||
	<slot></slot>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -38,13 +38,13 @@ const forceSpacerMin = inject('forceSpacerMin', false) || deviceKind === 'smartp
 | 
			
		||||
	container-type: inline-size;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 360px) {
 | 
			
		||||
@container (max-width: 450px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: v-bind('props.marginMin + "px"');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (min-width: 361px) {
 | 
			
		||||
@container (min-width: 451px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: v-bind('props.marginMax + "px"');
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,22 +36,21 @@ const relative = $computed(() => {
 | 
			
		||||
		i18n.ts._ago.future);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function tick() {
 | 
			
		||||
	// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
 | 
			
		||||
	now = new Date();
 | 
			
		||||
 | 
			
		||||
	tickId = window.setTimeout(() => {
 | 
			
		||||
		window.requestAnimationFrame(tick);
 | 
			
		||||
	}, 10000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let tickId: number;
 | 
			
		||||
 | 
			
		||||
function tick() {
 | 
			
		||||
	now = new Date();
 | 
			
		||||
	const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
 | 
			
		||||
	const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
 | 
			
		||||
 | 
			
		||||
	tickId = window.setTimeout(tick, next);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (props.mode === 'relative' || props.mode === 'detail') {
 | 
			
		||||
	tickId = window.requestAnimationFrame(tick);
 | 
			
		||||
	tick();
 | 
			
		||||
 | 
			
		||||
	onUnmounted(() => {
 | 
			
		||||
		window.cancelAnimationFrame(tickId);
 | 
			
		||||
		window.clearTimeout(tickId);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user