Merge branch 'develop' into mahjong
This commit is contained in:
		| @@ -385,7 +385,7 @@ export class CustomEmojiService implements OnApplicationShutdown { | ||||
| 	 */ | ||||
| 	@bindThis | ||||
| 	public checkDuplicate(name: string): Promise<boolean> { | ||||
| 		return this.emojisRepository.exist({ where: { name, host: IsNull() } }); | ||||
| 		return this.emojisRepository.exists({ where: { name, host: IsNull() } }); | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
|   | ||||
| @@ -419,6 +419,10 @@ export class MfmService { | ||||
| 			}, | ||||
|  | ||||
| 			text: (node) => { | ||||
| 				if (!node.props.text.match(/[\r\n]/)) { | ||||
| 					return doc.createTextNode(node.props.text); | ||||
| 				} | ||||
|  | ||||
| 				const el = doc.createElement('span'); | ||||
| 				const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); | ||||
|  | ||||
|   | ||||
| @@ -603,7 +603,7 @@ export class NoteCreateService implements OnApplicationShutdown { | ||||
| 			if (data.reply) { | ||||
| 				// 通知 | ||||
| 				if (data.reply.userHost === null) { | ||||
| 					const isThreadMuted = await this.noteThreadMutingsRepository.exist({ | ||||
| 					const isThreadMuted = await this.noteThreadMutingsRepository.exists({ | ||||
| 						where: { | ||||
| 							userId: data.reply.userId, | ||||
| 							threadId: data.reply.threadId ?? data.reply.id, | ||||
| @@ -741,7 +741,7 @@ export class NoteCreateService implements OnApplicationShutdown { | ||||
| 	@bindThis | ||||
| 	private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) { | ||||
| 		for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) { | ||||
| 			const isThreadMuted = await this.noteThreadMutingsRepository.exist({ | ||||
| 			const isThreadMuted = await this.noteThreadMutingsRepository.exists({ | ||||
| 				where: { | ||||
| 					userId: u.id, | ||||
| 					threadId: note.threadId ?? note.id, | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export class NoteReadService implements OnApplicationShutdown { | ||||
| 		//#endregion | ||||
|  | ||||
| 		// スレッドミュート | ||||
| 		const isThreadMuted = await this.noteThreadMutingsRepository.exist({ | ||||
| 		const isThreadMuted = await this.noteThreadMutingsRepository.exists({ | ||||
| 			where: { | ||||
| 				userId: userId, | ||||
| 				threadId: note.threadId ?? note.id, | ||||
| @@ -70,7 +70,7 @@ export class NoteReadService implements OnApplicationShutdown { | ||||
|  | ||||
| 		// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する | ||||
| 		setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => { | ||||
| 			const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } }); | ||||
| 			const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } }); | ||||
|  | ||||
| 			if (!exist) return; | ||||
|  | ||||
|   | ||||
| @@ -74,12 +74,12 @@ export class SignupService { | ||||
| 		const secret = generateUserToken(); | ||||
|  | ||||
| 		// Check username duplication | ||||
| 		if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { | ||||
| 		if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { | ||||
| 			throw new Error('DUPLICATED_USERNAME'); | ||||
| 		} | ||||
|  | ||||
| 		// Check deleted username duplication | ||||
| 		if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) { | ||||
| 		if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { | ||||
| 			throw new Error('USED_USERNAME'); | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -144,7 +144,7 @@ export class UserFollowingService implements OnModuleInit { | ||||
| 			let autoAccept = false; | ||||
|  | ||||
| 			// 鍵アカウントであっても、既にフォローされていた場合はスルー | ||||
| 			const isFollowing = await this.followingsRepository.exist({ | ||||
| 			const isFollowing = await this.followingsRepository.exists({ | ||||
| 				where: { | ||||
| 					followerId: follower.id, | ||||
| 					followeeId: followee.id, | ||||
| @@ -156,7 +156,7 @@ export class UserFollowingService implements OnModuleInit { | ||||
|  | ||||
| 			// フォローしているユーザーは自動承認オプション | ||||
| 			if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { | ||||
| 				const isFollowed = await this.followingsRepository.exist({ | ||||
| 				const isFollowed = await this.followingsRepository.exists({ | ||||
| 					where: { | ||||
| 						followerId: followee.id, | ||||
| 						followeeId: follower.id, | ||||
| @@ -170,7 +170,7 @@ export class UserFollowingService implements OnModuleInit { | ||||
| 			if (followee.isLocked && !autoAccept) { | ||||
| 				autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs( | ||||
| 					follower, | ||||
| 					(oldSrc, newSrc) => this.followingsRepository.exist({ | ||||
| 					(oldSrc, newSrc) => this.followingsRepository.exists({ | ||||
| 						where: { | ||||
| 							followeeId: followee.id, | ||||
| 							followerId: newSrc.id, | ||||
| @@ -233,7 +233,7 @@ export class UserFollowingService implements OnModuleInit { | ||||
|  | ||||
| 		this.cacheService.userFollowingsCache.refresh(follower.id); | ||||
|  | ||||
| 		const requestExist = await this.followRequestsRepository.exist({ | ||||
| 		const requestExist = await this.followRequestsRepository.exists({ | ||||
| 			where: { | ||||
| 				followeeId: followee.id, | ||||
| 				followerId: follower.id, | ||||
| @@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit { | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		const requestExist = await this.followRequestsRepository.exist({ | ||||
| 		const requestExist = await this.followRequestsRepository.exists({ | ||||
| 			where: { | ||||
| 				followeeId: followee.id, | ||||
| 				followerId: follower.id, | ||||
|   | ||||
| @@ -629,7 +629,7 @@ export class ApInboxService { | ||||
| 			return 'skip: follower not found'; | ||||
| 		} | ||||
|  | ||||
| 		const isFollowing = await this.followingsRepository.exist({ | ||||
| 		const isFollowing = await this.followingsRepository.exists({ | ||||
| 			where: { | ||||
| 				followerId: follower.id, | ||||
| 				followeeId: actor.id, | ||||
| @@ -686,14 +686,14 @@ export class ApInboxService { | ||||
| 			return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません'; | ||||
| 		} | ||||
|  | ||||
| 		const requestExist = await this.followRequestsRepository.exist({ | ||||
| 		const requestExist = await this.followRequestsRepository.exists({ | ||||
| 			where: { | ||||
| 				followerId: actor.id, | ||||
| 				followeeId: followee.id, | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
| 		const isFollowing = await this.followingsRepository.exist({ | ||||
| 		const isFollowing = await this.followingsRepository.exists({ | ||||
| 			where: { | ||||
| 				followerId: actor.id, | ||||
| 				followeeId: followee.id, | ||||
|   | ||||
| @@ -25,8 +25,21 @@ export class ApMfmService { | ||||
| 	} | ||||
|  | ||||
| 	@bindThis | ||||
| 	public getNoteHtml(note: MiNote): string | null { | ||||
| 		if (!note.text) return ''; | ||||
| 		return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); | ||||
| 	public getNoteHtml(note: MiNote, apAppend?: string) { | ||||
| 		let noMisskeyContent = false; | ||||
| 		const srcMfm = (note.text ?? '') + (apAppend ?? ''); | ||||
|  | ||||
| 		const parsed = mfm.parse(srcMfm); | ||||
|  | ||||
| 		if (!apAppend && parsed?.every(n => ['text', 'unicodeEmoji', 'emojiCode', 'mention', 'hashtag', 'url'].includes(n.type))) { | ||||
| 			noMisskeyContent = true; | ||||
| 		} | ||||
|  | ||||
| 		const content = this.mfmService.toHtml(parsed, JSON.parse(note.mentionedRemoteUsers)); | ||||
|  | ||||
| 		return { | ||||
| 			content, | ||||
| 			noMisskeyContent, | ||||
| 		}; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -325,7 +325,7 @@ export class ApRendererService { | ||||
| 			inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId }); | ||||
|  | ||||
| 			if (inReplyToNote != null) { | ||||
| 				const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } }); | ||||
| 				const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } }); | ||||
|  | ||||
| 				if (inReplyToUserExist) { | ||||
| 					if (inReplyToNote.uri) { | ||||
| @@ -389,17 +389,15 @@ export class ApRendererService { | ||||
| 			poll = await this.pollsRepository.findOneBy({ noteId: note.id }); | ||||
| 		} | ||||
|  | ||||
| 		let apText = text; | ||||
| 		let apAppend = ''; | ||||
|  | ||||
| 		if (quote) { | ||||
| 			apText += `\n\nRE: ${quote}`; | ||||
| 			apAppend += `\n\nRE: ${quote}`; | ||||
| 		} | ||||
|  | ||||
| 		const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; | ||||
|  | ||||
| 		const content = this.apMfmService.getNoteHtml(Object.assign({}, note, { | ||||
| 			text: apText, | ||||
| 		})); | ||||
| 		const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); | ||||
|  | ||||
| 		const emojis = await this.getEmojis(note.emojis); | ||||
| 		const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); | ||||
| @@ -412,9 +410,6 @@ export class ApRendererService { | ||||
|  | ||||
| 		const asPoll = poll ? { | ||||
| 			type: 'Question', | ||||
| 			content: this.apMfmService.getNoteHtml(Object.assign({}, note, { | ||||
| 				text: text, | ||||
| 			})), | ||||
| 			[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, | ||||
| 			[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ | ||||
| 				type: 'Note', | ||||
| @@ -432,11 +427,13 @@ export class ApRendererService { | ||||
| 			attributedTo, | ||||
| 			summary: summary ?? undefined, | ||||
| 			content: content ?? undefined, | ||||
| 			_misskey_content: text, | ||||
| 			source: { | ||||
| 				content: text, | ||||
| 				mediaType: 'text/x.misskeymarkdown', | ||||
| 			}, | ||||
| 			...(noMisskeyContent ? {} : { | ||||
| 				_misskey_content: text, | ||||
| 				source: { | ||||
| 					content: text, | ||||
| 					mediaType: 'text/x.misskeymarkdown', | ||||
| 				}, | ||||
| 			}), | ||||
| 			_misskey_quote: quote, | ||||
| 			quoteUrl: quote, | ||||
| 			published: this.idService.parse(note.id).date.toISOString(), | ||||
| @@ -625,6 +622,7 @@ export class ApRendererService { | ||||
| 				'https://www.w3.org/ns/activitystreams', | ||||
| 				'https://w3id.org/security/v1', | ||||
| 				{ | ||||
| 					Key: 'sec:Key', | ||||
| 					// as non-standards | ||||
| 					manuallyApprovesFollowers: 'as:manuallyApprovesFollowers', | ||||
| 					sensitive: 'as:sensitive', | ||||
|   | ||||
| @@ -51,14 +51,14 @@ export class ChannelEntityService { | ||||
|  | ||||
| 		const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null; | ||||
|  | ||||
| 		const isFollowing = meId ? await this.channelFollowingsRepository.exist({ | ||||
| 		const isFollowing = meId ? await this.channelFollowingsRepository.exists({ | ||||
| 			where: { | ||||
| 				followerId: meId, | ||||
| 				followeeId: channel.id, | ||||
| 			}, | ||||
| 		}) : false; | ||||
|  | ||||
| 		const isFavorited = meId ? await this.channelFavoritesRepository.exist({ | ||||
| 		const isFavorited = meId ? await this.channelFavoritesRepository.exists({ | ||||
| 			where: { | ||||
| 				userId: meId, | ||||
| 				channelId: channel.id, | ||||
|   | ||||
| @@ -46,7 +46,7 @@ export class ClipEntityService { | ||||
| 			description: clip.description, | ||||
| 			isPublic: clip.isPublic, | ||||
| 			favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }), | ||||
| 			isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined, | ||||
| 			isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -31,6 +31,7 @@ export class EmojiEntityService { | ||||
| 			category: emoji.category, | ||||
| 			// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ) | ||||
| 			url: emoji.publicUrl || emoji.originalUrl, | ||||
| 			localOnly: emoji.localOnly ? true : undefined, | ||||
| 			isSensitive: emoji.isSensitive ? true : undefined, | ||||
| 			roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, | ||||
| 		}; | ||||
|   | ||||
| @@ -47,7 +47,7 @@ export class FlashEntityService { | ||||
| 			summary: flash.summary, | ||||
| 			script: flash.script, | ||||
| 			likedCount: flash.likedCount, | ||||
| 			isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined, | ||||
| 			isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -53,7 +53,7 @@ export class GalleryPostEntityService { | ||||
| 			tags: post.tags.length > 0 ? post.tags : undefined, | ||||
| 			isSensitive: post.isSensitive, | ||||
| 			likedCount: post.likedCount, | ||||
| 			isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined, | ||||
| 			isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -108,7 +108,7 @@ export class NoteEntityService implements OnModuleInit { | ||||
| 				hide = false; | ||||
| 			} else { | ||||
| 				// フォロワーかどうか | ||||
| 				const isFollowing = await this.followingsRepository.exist({ | ||||
| 				const isFollowing = await this.followingsRepository.exists({ | ||||
| 					where: { | ||||
| 						followeeId: packedNote.userId, | ||||
| 						followerId: meId, | ||||
|   | ||||
| @@ -104,7 +104,7 @@ export class PageEntityService { | ||||
| 			eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null, | ||||
| 			attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)), | ||||
| 			likedCount: page.likedCount, | ||||
| 			isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined, | ||||
| 			isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -152,43 +152,43 @@ export class UserEntityService implements OnModuleInit { | ||||
| 				followerId: me, | ||||
| 				followeeId: target, | ||||
| 			}), | ||||
| 			this.followingsRepository.exist({ | ||||
| 			this.followingsRepository.exists({ | ||||
| 				where: { | ||||
| 					followerId: target, | ||||
| 					followeeId: me, | ||||
| 				}, | ||||
| 			}), | ||||
| 			this.followRequestsRepository.exist({ | ||||
| 			this.followRequestsRepository.exists({ | ||||
| 				where: { | ||||
| 					followerId: me, | ||||
| 					followeeId: target, | ||||
| 				}, | ||||
| 			}), | ||||
| 			this.followRequestsRepository.exist({ | ||||
| 			this.followRequestsRepository.exists({ | ||||
| 				where: { | ||||
| 					followerId: target, | ||||
| 					followeeId: me, | ||||
| 				}, | ||||
| 			}), | ||||
| 			this.blockingsRepository.exist({ | ||||
| 			this.blockingsRepository.exists({ | ||||
| 				where: { | ||||
| 					blockerId: me, | ||||
| 					blockeeId: target, | ||||
| 				}, | ||||
| 			}), | ||||
| 			this.blockingsRepository.exist({ | ||||
| 			this.blockingsRepository.exists({ | ||||
| 				where: { | ||||
| 					blockerId: target, | ||||
| 					blockeeId: me, | ||||
| 				}, | ||||
| 			}), | ||||
| 			this.mutingsRepository.exist({ | ||||
| 			this.mutingsRepository.exists({ | ||||
| 				where: { | ||||
| 					muterId: me, | ||||
| 					muteeId: target, | ||||
| 				}, | ||||
| 			}), | ||||
| 			this.renoteMutingsRepository.exist({ | ||||
| 			this.renoteMutingsRepository.exists({ | ||||
| 				where: { | ||||
| 					muterId: me, | ||||
| 					muteeId: target, | ||||
| @@ -215,7 +215,7 @@ export class UserEntityService implements OnModuleInit { | ||||
| 		/* | ||||
| 		const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId); | ||||
|  | ||||
| 		const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({ | ||||
| 		const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exists({ | ||||
| 			where: { | ||||
| 				antennaId: In(myAntennas.map(x => x.id)), | ||||
| 				read: false, | ||||
|   | ||||
| @@ -27,6 +27,10 @@ export const packedEmojiSimpleSchema = { | ||||
| 			type: 'string', | ||||
| 			optional: false, nullable: false, | ||||
| 		}, | ||||
| 		localOnly: { | ||||
| 			type: 'boolean', | ||||
| 			optional: true, nullable: false, | ||||
| 		}, | ||||
| 		isSensitive: { | ||||
| 			type: 'boolean', | ||||
| 			optional: true, nullable: false, | ||||
|   | ||||
| @@ -163,12 +163,12 @@ export class SignupApiService { | ||||
| 		} | ||||
|  | ||||
| 		if (instance.emailRequiredForSignup) { | ||||
| 			if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { | ||||
| 			if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { | ||||
| 				throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); | ||||
| 			} | ||||
|  | ||||
| 			// Check deleted username duplication | ||||
| 			if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) { | ||||
| 			if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) { | ||||
| 				throw new FastifyReplyError(400, 'USED_USERNAME'); | ||||
| 			} | ||||
|  | ||||
|   | ||||
| @@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				throw e; | ||||
| 			}); | ||||
|  | ||||
| 			const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } }); | ||||
| 			const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } }); | ||||
|  | ||||
| 			if (exist) { | ||||
| 				throw new ApiError(meta.errors.alreadyPromoted); | ||||
|   | ||||
| @@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			const accessToken = secureRndstr(32); | ||||
|  | ||||
| 			// Fetch exist access token | ||||
| 			const exist = await this.accessTokensRepository.exist({ | ||||
| 			const exist = await this.accessTokensRepository.exists({ | ||||
| 				where: { | ||||
| 					appId: session.appId, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			}); | ||||
|  | ||||
| 			// Check if already blocking | ||||
| 			const exist = await this.blockingsRepository.exist({ | ||||
| 			const exist = await this.blockingsRepository.exists({ | ||||
| 				where: { | ||||
| 					blockerId: blocker.id, | ||||
| 					blockeeId: blockee.id, | ||||
|   | ||||
| @@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			}); | ||||
|  | ||||
| 			// Check not blocking | ||||
| 			const exist = await this.blockingsRepository.exist({ | ||||
| 			const exist = await this.blockingsRepository.exists({ | ||||
| 				where: { | ||||
| 					blockerId: blocker.id, | ||||
| 					blockeeId: blockee.id, | ||||
|   | ||||
| @@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				throw new ApiError(meta.errors.noSuchClip); | ||||
| 			} | ||||
|  | ||||
| 			const exist = await this.clipFavoritesRepository.exist({ | ||||
| 			const exist = await this.clipFavoritesRepository.exists({ | ||||
| 				where: { | ||||
| 					clipId: clip.id, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -38,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 		private driveFilesRepository: DriveFilesRepository, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const exist = await this.driveFilesRepository.exist({ | ||||
| 			const exist = await this.driveFilesRepository.exists({ | ||||
| 				where: { | ||||
| 					md5: ps.md5, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			} | ||||
|  | ||||
| 			// if already liked | ||||
| 			const exist = await this.flashLikesRepository.exist({ | ||||
| 			const exist = await this.flashLikesRepository.exists({ | ||||
| 				where: { | ||||
| 					flashId: flash.id, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			}); | ||||
|  | ||||
| 			// Check if already following | ||||
| 			const exist = await this.followingsRepository.exist({ | ||||
| 			const exist = await this.followingsRepository.exists({ | ||||
| 				where: { | ||||
| 					followerId: follower.id, | ||||
| 					followeeId: followee.id, | ||||
|   | ||||
| @@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			}); | ||||
|  | ||||
| 			// Check not following | ||||
| 			const exist = await this.followingsRepository.exist({ | ||||
| 			const exist = await this.followingsRepository.exists({ | ||||
| 				where: { | ||||
| 					followerId: follower.id, | ||||
| 					followeeId: followee.id, | ||||
|   | ||||
| @@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			} | ||||
|  | ||||
| 			// if already liked | ||||
| 			const exist = await this.galleryLikesRepository.exist({ | ||||
| 			const exist = await this.galleryLikesRepository.exists({ | ||||
| 				where: { | ||||
| 					postId: post.id, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private downloadService: DownloadService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const userExist = await this.usersRepository.exist({ where: { id: me.id } }); | ||||
| 			const userExist = await this.usersRepository.exists({ where: { id: me.id } }); | ||||
| 			if (!userExist) throw new ApiError(meta.errors.noSuchUser); | ||||
| 			const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); | ||||
| 			if (file === null) throw new ApiError(meta.errors.noSuchFile); | ||||
|   | ||||
| @@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			if (ps.tokenId) { | ||||
| 				const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } }); | ||||
| 				const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } }); | ||||
|  | ||||
| 				if (tokenExist) { | ||||
| 					await this.accessTokensRepository.delete({ | ||||
| @@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 					}); | ||||
| 				} | ||||
| 			} else if (ps.token) { | ||||
| 				const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } }); | ||||
| 				const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } }); | ||||
|  | ||||
| 				if (tokenExist) { | ||||
| 					await this.accessTokensRepository.delete({ | ||||
|   | ||||
| @@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			}); | ||||
|  | ||||
| 			// Check if already muting | ||||
| 			const exist = await this.mutingsRepository.exist({ | ||||
| 			const exist = await this.mutingsRepository.exists({ | ||||
| 				where: { | ||||
| 					muterId: muter.id, | ||||
| 					muteeId: mutee.id, | ||||
|   | ||||
| @@ -260,7 +260,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
|  | ||||
| 				// Check blocking | ||||
| 				if (renote.userId !== me.id) { | ||||
| 					const blockExist = await this.blockingsRepository.exist({ | ||||
| 					const blockExist = await this.blockingsRepository.exists({ | ||||
| 						where: { | ||||
| 							blockerId: renote.userId, | ||||
| 							blockeeId: me.id, | ||||
| @@ -308,7 +308,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
|  | ||||
| 				// Check blocking | ||||
| 				if (reply.userId !== me.id) { | ||||
| 					const blockExist = await this.blockingsRepository.exist({ | ||||
| 					const blockExist = await this.blockingsRepository.exists({ | ||||
| 						where: { | ||||
| 							blockerId: reply.userId, | ||||
| 							blockeeId: me.id, | ||||
|   | ||||
| @@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			}); | ||||
|  | ||||
| 			// if already favorited | ||||
| 			const exist = await this.noteFavoritesRepository.exist({ | ||||
| 			const exist = await this.noteFavoritesRepository.exists({ | ||||
| 				where: { | ||||
| 					noteId: note.id, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 			} | ||||
|  | ||||
| 			// if already liked | ||||
| 			const exist = await this.pageLikesRepository.exist({ | ||||
| 			const exist = await this.pageLikesRepository.exists({ | ||||
| 				where: { | ||||
| 					pageId: page.id, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				throw err; | ||||
| 			}); | ||||
|  | ||||
| 			const exist = await this.promoReadsRepository.exist({ | ||||
| 			const exist = await this.promoReadsRepository.exists({ | ||||
| 				where: { | ||||
| 					noteId: note.id, | ||||
| 					userId: me.id, | ||||
|   | ||||
| @@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				if (me == null) { | ||||
| 					throw new ApiError(meta.errors.forbidden); | ||||
| 				} else if (me.id !== user.id) { | ||||
| 					const isFollowing = await this.followingsRepository.exist({ | ||||
| 					const isFollowing = await this.followingsRepository.exists({ | ||||
| 						where: { | ||||
| 							followeeId: user.id, | ||||
| 							followerId: me.id, | ||||
|   | ||||
| @@ -109,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				if (me == null) { | ||||
| 					throw new ApiError(meta.errors.forbidden); | ||||
| 				} else if (me.id !== user.id) { | ||||
| 					const isFollowing = await this.followingsRepository.exist({ | ||||
| 					const isFollowing = await this.followingsRepository.exists({ | ||||
| 						where: { | ||||
| 							followeeId: user.id, | ||||
| 							followerId: me.id, | ||||
|   | ||||
| @@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 		private roleService: RoleService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const listExist = await this.userListsRepository.exist({ | ||||
| 			const listExist = await this.userListsRepository.exists({ | ||||
| 				where: { | ||||
| 					id: ps.listId, | ||||
| 					isPublic: true, | ||||
| @@ -121,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				}); | ||||
|  | ||||
| 				if (currentUser.id !== me.id) { | ||||
| 					const blockExist = await this.blockingsRepository.exist({ | ||||
| 					const blockExist = await this.blockingsRepository.exists({ | ||||
| 						where: { | ||||
| 							blockerId: currentUser.id, | ||||
| 							blockeeId: me.id, | ||||
| @@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				const exist = await this.userListMembershipsRepository.exist({ | ||||
| 				const exist = await this.userListMembershipsRepository.exists({ | ||||
| 					where: { | ||||
| 						userListId: userList.id, | ||||
| 						userId: currentUser.id, | ||||
|   | ||||
| @@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private idService: IdService, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const userListExist = await this.userListsRepository.exist({ | ||||
| 			const userListExist = await this.userListsRepository.exists({ | ||||
| 				where: { | ||||
| 					id: ps.listId, | ||||
| 					isPublic: true, | ||||
| @@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 				throw new ApiError(meta.errors.noSuchList); | ||||
| 			} | ||||
|  | ||||
| 			const exist = await this.userListFavoritesRepository.exist({ | ||||
| 			const exist = await this.userListFavoritesRepository.exists({ | ||||
| 				where: { | ||||
| 					userId: me.id, | ||||
| 					userListId: ps.listId, | ||||
|   | ||||
| @@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
|  | ||||
| 			// Check blocking | ||||
| 			if (user.id !== me.id) { | ||||
| 				const blockExist = await this.blockingsRepository.exist({ | ||||
| 				const blockExist = await this.blockingsRepository.exists({ | ||||
| 					where: { | ||||
| 						blockerId: user.id, | ||||
| 						blockeeId: me.id, | ||||
| @@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			const exist = await this.userListMembershipsRepository.exist({ | ||||
| 			const exist = await this.userListMembershipsRepository.exists({ | ||||
| 				where: { | ||||
| 					userListId: userList.id, | ||||
| 					userId: user.id, | ||||
|   | ||||
| @@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 					userListId: ps.listId, | ||||
| 				}); | ||||
| 				if (me !== null) { | ||||
| 					additionalProperties.isLiked = await this.userListFavoritesRepository.exist({ | ||||
| 					additionalProperties.isLiked = await this.userListFavoritesRepository.exists({ | ||||
| 						where: { | ||||
| 							userId: me.id, | ||||
| 							userListId: ps.listId, | ||||
|   | ||||
| @@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||
| 		private userListFavoritesRepository: UserListFavoritesRepository, | ||||
| 	) { | ||||
| 		super(meta, paramDef, async (ps, me) => { | ||||
| 			const userListExist = await this.userListsRepository.exist({ | ||||
| 			const userListExist = await this.userListsRepository.exists({ | ||||
| 				where: { | ||||
| 					id: ps.listId, | ||||
| 					isPublic: true, | ||||
|   | ||||
| @@ -43,7 +43,7 @@ class UserListChannel extends Channel { | ||||
| 		this.withRenotes = params.withRenotes ?? true; | ||||
|  | ||||
| 		// Check existence and owner | ||||
| 		const listExist = await this.userListsRepository.exist({ | ||||
| 		const listExist = await this.userListsRepository.exists({ | ||||
| 			where: { | ||||
| 				id: this.listId, | ||||
| 				userId: this.user!.id, | ||||
|   | ||||
							
								
								
									
										44
									
								
								packages/backend/test/unit/ApMfmService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/backend/test/unit/ApMfmService.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| import * as assert from 'assert'; | ||||
| import { Test } from '@nestjs/testing'; | ||||
|  | ||||
| import { CoreModule } from '@/core/CoreModule.js'; | ||||
| import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; | ||||
| import { GlobalModule } from '@/GlobalModule.js'; | ||||
| import { MiNote } from '@/models/Note.js'; | ||||
|  | ||||
| describe('ApMfmService', () => { | ||||
| 	let apMfmService: ApMfmService; | ||||
|  | ||||
| 	beforeAll(async () => { | ||||
| 		const app = await Test.createTestingModule({ | ||||
| 			imports: [GlobalModule, CoreModule], | ||||
| 		}).compile(); | ||||
| 		apMfmService = app.get<ApMfmService>(ApMfmService); | ||||
| 	}); | ||||
|  | ||||
| 	describe('getNoteHtml', () => { | ||||
| 		test('Do not provide _misskey_content for simple text', () => { | ||||
| 			const note: MiNote = { | ||||
| 				text: 'テキスト #タグ @mention 🍊 :emoji: https://example.com', | ||||
| 				mentionedRemoteUsers: '[]', | ||||
| 			} as any; | ||||
|  | ||||
| 			const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); | ||||
|  | ||||
| 			assert.equal(noMisskeyContent, true, 'noMisskeyContent'); | ||||
| 			assert.equal(content, '<p>テキスト <a href="http://misskey.local/tags/タグ" rel="tag">#タグ</a> <a href="http://misskey.local/@mention" class="u-url mention">@mention</a> 🍊 :emoji: <a href="https://example.com">https://example.com</a></p>', 'content'); | ||||
| 		}); | ||||
|  | ||||
| 		test('Provide _misskey_content for MFM', () => { | ||||
| 			const note: MiNote = { | ||||
| 				text: '$[tada foo]', | ||||
| 				mentionedRemoteUsers: '[]', | ||||
| 			} as any; | ||||
|  | ||||
| 			const { content, noMisskeyContent } = apMfmService.getNoteHtml(note); | ||||
|  | ||||
| 			assert.equal(noMisskeyContent, false, 'noMisskeyContent'); | ||||
| 			assert.equal(content, '<p><i>foo</i></p>', 'content'); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
| @@ -33,6 +33,12 @@ describe('MfmService', () => { | ||||
| 			const output = '<p><span>foo<br>bar<br>baz</span></p>'; | ||||
| 			assert.equal(mfmService.toHtml(mfm.parse(input)), output); | ||||
| 		}); | ||||
|  | ||||
| 		test('Do not generate unnecessary span', () => { | ||||
| 			const input = 'foo $[tada bar]'; | ||||
| 			const output = '<p>foo <i>bar</i></p>'; | ||||
| 			assert.equal(mfmService.toHtml(mfm.parse(input)), output); | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| 	describe('fromHtml', () => { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
|  | ||||
| import { type SharedOptions, rest } from 'msw'; | ||||
| import { type SharedOptions, http, HttpResponse } from 'msw'; | ||||
|  | ||||
| export const onUnhandledRequest = ((req, print) => { | ||||
| 	if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) { | ||||
| @@ -13,19 +13,31 @@ export const onUnhandledRequest = ((req, print) => { | ||||
| }) satisfies SharedOptions['onUnhandledRequest']; | ||||
|  | ||||
| export const commonHandlers = [ | ||||
| 	rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => { | ||||
| 		const { codepoints } = req.params; | ||||
| 	http.get('/fluent-emoji/:codepoints.png', async ({ params }) => { | ||||
| 		const { codepoints } = params; | ||||
| 		const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); | ||||
| 		return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); | ||||
| 		return new HttpResponse(value, { | ||||
| 			headers: { | ||||
| 				'Content-Type': 'image/png', | ||||
| 			}, | ||||
| 		}); | ||||
| 	}), | ||||
| 	rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => { | ||||
| 		const { codepoints } = req.params; | ||||
| 	http.get('/fluent-emojis/:codepoints.png', async ({ params }) => { | ||||
| 		const { codepoints } = params; | ||||
| 		const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob()); | ||||
| 		return res(ctx.set('Content-Type', 'image/png'), ctx.body(value)); | ||||
| 		return new HttpResponse(value, { | ||||
| 			headers: { | ||||
| 				'Content-Type': 'image/png', | ||||
| 			}, | ||||
| 		}); | ||||
| 	}), | ||||
| 	rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => { | ||||
| 		const { codepoints } = req.params; | ||||
| 	http.get('/twemoji/:codepoints.svg', async ({ params }) => { | ||||
| 		const { codepoints } = params; | ||||
| 		const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob()); | ||||
| 		return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value)); | ||||
| 		return new HttpResponse(value, { | ||||
| 			headers: { | ||||
| 				'Content-Type': 'image/svg+xml', | ||||
| 			}, | ||||
| 		}); | ||||
| 	}), | ||||
| ]; | ||||
|   | ||||
| @@ -123,8 +123,8 @@ | ||||
| 		"happy-dom": "10.0.3", | ||||
| 		"intersection-observer": "0.12.2", | ||||
| 		"micromatch": "4.0.5", | ||||
| 		"msw": "2.1.2", | ||||
| 		"msw-storybook-addon": "1.10.0", | ||||
| 		"msw": "2.1.7", | ||||
| 		"msw-storybook-addon": "2.0.0-beta.1", | ||||
| 		"nodemon": "3.0.3", | ||||
| 		"prettier": "3.2.4", | ||||
| 		"react": "18.2.0", | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { abuseUserReport } from '../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import MkAbuseReport from './MkAbuseReport.vue'; | ||||
| @@ -44,9 +44,9 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => { | ||||
| 					action('POST /api/admin/resolve-abuse-user-report')(await req.json()); | ||||
| 					return res(ctx.json({})); | ||||
| 				http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => { | ||||
| 					action('POST /api/admin/resolve-abuse-user-report')(await request.json()); | ||||
| 					return HttpResponse.json({}); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { userDetailed } from '../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import MkAbuseReportWindow from './MkAbuseReportWindow.vue'; | ||||
| @@ -44,9 +44,9 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/report-abuse', async (req, res, ctx) => { | ||||
| 					action('POST /api/users/report-abuse')(await req.json()); | ||||
| 					return res(ctx.json({})); | ||||
| 				http.post('/api/users/report-abuse', async ({ request }) => { | ||||
| 					action('POST /api/users/report-abuse')(await request.json()); | ||||
| 					return HttpResponse.json({}); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { userDetailed } from '../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import MkAchievements from './MkAchievements.vue'; | ||||
| @@ -39,8 +39,8 @@ export const Empty = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/achievements', (req, res, ctx) => { | ||||
| 					return res(ctx.json([])); | ||||
| 				http.post('/api/users/achievements', () => { | ||||
| 					return HttpResponse.json([]); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
| @@ -52,8 +52,8 @@ export const All = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/achievements', (req, res, ctx) => { | ||||
| 					return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })))); | ||||
| 				http.post('/api/users/achievements', () => { | ||||
| 					return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; | ||||
| import { expect } from '@storybook/jest'; | ||||
| import { userEvent, waitFor, within } from '@storybook/testing-library'; | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { userDetailed } from '../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import MkAutocomplete from './MkAutocomplete.vue'; | ||||
| @@ -99,11 +99,11 @@ export const User = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/users/search-by-username-and-host', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'), | ||||
| 						userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'), | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
| @@ -132,12 +132,12 @@ export const Hashtag = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/hashtags/search', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/hashtags/search', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						'気象警報注意報', | ||||
| 						'気象警報', | ||||
| 						'気象情報', | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { userDetailed } from '../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import MkAvatars from './MkAvatars.vue'; | ||||
| @@ -38,12 +38,12 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/show', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/users/show', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						userDetailed('17'), | ||||
| 						userDetailed('20'), | ||||
| 						userDetailed('18'), | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -5,14 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <!-- eslint-disable vue/no-v-html --> | ||||
| <template> | ||||
| <div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div> | ||||
| <div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, computed, watch } from 'vue'; | ||||
| import { bundledLanguagesInfo } from 'shiki'; | ||||
| import type { BuiltinLanguage } from 'shiki'; | ||||
| import { getHighlighter } from '@/scripts/code-highlighter.js'; | ||||
| import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	code: string; | ||||
| @@ -21,11 +22,23 @@ const props = defineProps<{ | ||||
| }>(); | ||||
|  | ||||
| const highlighter = await getHighlighter(); | ||||
|  | ||||
| const darkMode = defaultStore.reactiveState.darkMode; | ||||
| const codeLang = ref<BuiltinLanguage | 'aiscript'>('js'); | ||||
|  | ||||
| const [lightThemeName, darkThemeName] = await Promise.all([ | ||||
| 	getTheme('light', true), | ||||
| 	getTheme('dark', true), | ||||
| ]); | ||||
|  | ||||
| const html = computed(() => highlighter.codeToHtml(props.code, { | ||||
| 	lang: codeLang.value, | ||||
| 	theme: 'dark-plus', | ||||
| 	themes: { | ||||
| 		fallback: 'dark-plus', | ||||
| 		light: lightThemeName, | ||||
| 		dark: darkThemeName, | ||||
| 	}, | ||||
| 	defaultColor: false, | ||||
| 	cssVariablePrefix: '--shiki-', | ||||
| })); | ||||
|  | ||||
| async function fetchLanguage(to: string): Promise<void> { | ||||
| @@ -64,6 +77,16 @@ watch(() => props.lang, (to) => { | ||||
| 	margin: .5em 0; | ||||
| 	overflow: auto; | ||||
| 	border-radius: 8px; | ||||
| 	border: 1px solid var(--divider); | ||||
| 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; | ||||
|  | ||||
| 	color: var(--shiki-fallback); | ||||
| 	background-color: var(--shiki-fallback-bg); | ||||
|  | ||||
| 	& span { | ||||
| 		color: var(--shiki-fallback); | ||||
| 		background-color: var(--shiki-fallback-bg); | ||||
| 	} | ||||
|  | ||||
| 	& pre, | ||||
| 	& code { | ||||
| @@ -71,6 +94,26 @@ watch(() => props.lang, (to) => { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .light.codeBlockRoot :global(.shiki) { | ||||
| 	color: var(--shiki-light); | ||||
| 	background-color: var(--shiki-light-bg); | ||||
|  | ||||
| 	& span { | ||||
| 		color: var(--shiki-light); | ||||
| 		background-color: var(--shiki-light-bg); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .dark.codeBlockRoot :global(.shiki) { | ||||
| 	color: var(--shiki-dark); | ||||
| 	background-color: var(--shiki-dark-bg); | ||||
|  | ||||
| 	& span { | ||||
| 		color: var(--shiki-dark); | ||||
| 		background-color: var(--shiki-dark-bg); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .codeBlockRoot.codeEditor { | ||||
| 	min-width: 100%; | ||||
| 	height: 100%; | ||||
| @@ -79,6 +122,7 @@ watch(() => props.lang, (to) => { | ||||
| 		padding: 12px; | ||||
| 		margin: 0; | ||||
| 		border-radius: 6px; | ||||
| 		border: none; | ||||
| 		min-height: 130px; | ||||
| 		pointer-events: none; | ||||
| 		min-width: calc(100% - 24px); | ||||
| @@ -90,6 +134,11 @@ watch(() => props.lang, (to) => { | ||||
| 		text-rendering: inherit; | ||||
|     text-transform: inherit; | ||||
|     white-space: pre; | ||||
|  | ||||
| 		& span { | ||||
| 			display: inline-block; | ||||
| 			min-height: 1em; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -53,7 +53,6 @@ function copy() { | ||||
| } | ||||
|  | ||||
| .codeBlockCopyButton { | ||||
| 	color: #D4D4D4; | ||||
| 	position: absolute; | ||||
| 	top: 8px; | ||||
| 	right: 8px; | ||||
| @@ -67,8 +66,7 @@ function copy() { | ||||
| .codeBlockFallbackRoot { | ||||
| 	display: block; | ||||
| 	overflow-wrap: anywhere; | ||||
| 	color: #D4D4D4; | ||||
| 	background: #1E1E1E; | ||||
| 	background: var(--bg); | ||||
| 	padding: 1em; | ||||
| 	margin: .5em 0; | ||||
| 	overflow: auto; | ||||
| @@ -93,8 +91,8 @@ function copy() { | ||||
| 	border-radius: 8px; | ||||
| 	padding: 24px; | ||||
| 	margin-top: 4px; | ||||
| 	color: #D4D4D4; | ||||
| 	background: #1E1E1E; | ||||
| 	color: var(--fg); | ||||
| 	background: var(--bg); | ||||
| } | ||||
|  | ||||
| .codePlaceholderContainer { | ||||
|   | ||||
| @@ -196,10 +196,11 @@ watch(v, newValue => { | ||||
| 	resize: none; | ||||
| 	text-align: left; | ||||
| 	color: transparent; | ||||
| 	caret-color: rgb(225, 228, 232); | ||||
| 	caret-color: var(--fg); | ||||
| 	background-color: transparent; | ||||
| 	border: 0; | ||||
| 	border-radius: 6px; | ||||
| 	box-sizing: border-box; | ||||
| 	outline: 0; | ||||
| 	min-width: calc(100% - 24px); | ||||
| 	height: 100%; | ||||
| @@ -210,6 +211,6 @@ watch(v, newValue => { | ||||
| } | ||||
|  | ||||
| .textarea::selection { | ||||
| 	color: #fff; | ||||
| 	color: var(--bg); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -18,8 +18,7 @@ const props = defineProps<{ | ||||
| 	display: inline-block; | ||||
| 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace; | ||||
| 	overflow-wrap: anywhere; | ||||
| 	color: #D4D4D4; | ||||
| 	background: #1E1E1E; | ||||
| 	background: var(--bg); | ||||
| 	padding: .1em; | ||||
| 	border-radius: .3em; | ||||
| } | ||||
|   | ||||
| @@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js'; | ||||
| import { $i } from '@/account.js'; | ||||
| import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	showPinned?: boolean; | ||||
| @@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{ | ||||
| 	asDrawer?: boolean; | ||||
| 	asWindow?: boolean; | ||||
| 	asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう | ||||
| 	targetNote?: Misskey.entities.Note; | ||||
| }>(), { | ||||
| 	showPinned: true, | ||||
| }); | ||||
| @@ -340,7 +342,7 @@ watch(q, () => { | ||||
| }); | ||||
|  | ||||
| function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { | ||||
| 	return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false; | ||||
| 	return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji); | ||||
| } | ||||
|  | ||||
| function focus() { | ||||
|   | ||||
| @@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 		:showPinned="showPinned" | ||||
| 		:pinnedEmojis="pinnedEmojis" | ||||
| 		:asReactionPicker="asReactionPicker" | ||||
| 		:targetNote="targetNote" | ||||
| 		:asDrawer="type === 'drawer'" | ||||
| 		:max-height="maxHeight" | ||||
| 		@chosen="chosen" | ||||
| @@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { shallowRef } from 'vue'; | ||||
| import MkModal from '@/components/MkModal.vue'; | ||||
| import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; | ||||
| @@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{ | ||||
| 	showPinned?: boolean; | ||||
|   pinnedEmojis?: string[], | ||||
| 	asReactionPicker?: boolean; | ||||
| 	targetNote?: Misskey.entities.Note; | ||||
|   choseAndClose?: boolean; | ||||
| }>(), { | ||||
| 	manualShowing: null, | ||||
|   | ||||
| @@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	:front="true" | ||||
| 	@closed="emit('closed')" | ||||
| > | ||||
| 	<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/> | ||||
| 	<MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/> | ||||
| </MkWindow> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import MkWindow from '@/components/MkWindow.vue'; | ||||
| import MkEmojiPicker from '@/components/MkEmojiPicker.vue'; | ||||
|  | ||||
| @@ -26,6 +27,7 @@ withDefaults(defineProps<{ | ||||
| 	src?: HTMLElement; | ||||
| 	showPinned?: boolean; | ||||
| 	asReactionPicker?: boolean; | ||||
| 	targetNote?: Misskey.entities.Note | ||||
| }>(), { | ||||
| 	showPinned: true, | ||||
| }); | ||||
|   | ||||
| @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	</Transition> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, shallowRef, computed, nextTick, watch } from 'vue'; | ||||
| import type { Tab } from '@/components/global/MkPageHeader.tabs.vue'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js'; | ||||
|  | ||||
| const rootEl = shallowRef<HTMLDivElement>(); | ||||
|  | ||||
| @@ -49,16 +49,16 @@ const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontal | ||||
| // ▼ しきい値 ▼ // | ||||
|  | ||||
| // スワイプと判定される最小の距離 | ||||
| const MIN_SWIPE_DISTANCE = 50; | ||||
| const MIN_SWIPE_DISTANCE = 20; | ||||
|  | ||||
| // スワイプ時の動作を発火する最小の距離 | ||||
| const SWIPE_DISTANCE_THRESHOLD = 125; | ||||
| const SWIPE_DISTANCE_THRESHOLD = 70; | ||||
|  | ||||
| // スワイプを中断するY方向の移動距離 | ||||
| const SWIPE_ABORT_Y_THRESHOLD = 75; | ||||
|  | ||||
| // スワイプできる最大の距離 | ||||
| const MAX_SWIPE_DISTANCE = 150; | ||||
| const MAX_SWIPE_DISTANCE = 120; | ||||
|  | ||||
| // ▲ しきい値 ▲ // | ||||
|  | ||||
| @@ -68,7 +68,6 @@ let startScreenY: number | null = null; | ||||
| const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value)); | ||||
|  | ||||
| const pullDistance = ref(0); | ||||
| const isSwiping = ref(false); | ||||
| const isSwipingForClass = ref(false); | ||||
| let swipeAborted = false; | ||||
|  | ||||
| @@ -77,6 +76,8 @@ function touchStart(event: TouchEvent) { | ||||
|  | ||||
| 	if (event.touches.length !== 1) return; | ||||
|  | ||||
| 	if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return; | ||||
|  | ||||
| 	startScreenX = event.touches[0].screenX; | ||||
| 	startScreenY = event.touches[0].screenY; | ||||
| } | ||||
| @@ -90,6 +91,8 @@ function touchMove(event: TouchEvent) { | ||||
|  | ||||
| 	if (swipeAborted) return; | ||||
|  | ||||
| 	if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return; | ||||
|  | ||||
| 	let distanceX = event.touches[0].screenX - startScreenX; | ||||
| 	let distanceY = event.touches[0].screenY - startScreenY; | ||||
|  | ||||
| @@ -139,6 +142,8 @@ function touchEnd(event: TouchEvent) { | ||||
|  | ||||
| 	if (!isSwiping.value) return; | ||||
|  | ||||
| 	if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return; | ||||
|  | ||||
| 	const distance = event.changedTouches[0].screenX - startScreenX; | ||||
|  | ||||
| 	if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) { | ||||
| @@ -162,6 +167,24 @@ function touchEnd(event: TouchEvent) { | ||||
| 	}, 400); | ||||
| } | ||||
|  | ||||
| /** 横スワイプに関与する可能性のある要素を調べる */ | ||||
| function hasSomethingToDoWithXSwipe(el: HTMLElement) { | ||||
| 	if (['INPUT', 'TEXTAREA'].includes(el.tagName)) return true; | ||||
| 	if (el.isContentEditable) return true; | ||||
| 	if (el.scrollWidth > el.clientWidth) return true; | ||||
|  | ||||
| 	const style = window.getComputedStyle(el); | ||||
| 	if (['absolute', 'fixed', 'sticky'].includes(style.position)) return true; | ||||
| 	if (['scroll', 'auto'].includes(style.overflowX)) return true; | ||||
| 	if (style.touchAction === 'pan-x') return true; | ||||
|  | ||||
| 	if (el.parentElement && el.parentElement !== rootEl.value) { | ||||
| 		return hasSomethingToDoWithXSwipe(el.parentElement); | ||||
| 	} else { | ||||
| 		return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined); | ||||
|  | ||||
| watch(tabModel, (newTab, oldTab) => { | ||||
| @@ -182,6 +205,7 @@ watch(tabModel, (newTab, oldTab) => { | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .transitionRoot { | ||||
| 	touch-action: pan-y pinch-zoom; | ||||
| 	display: grid; | ||||
| 	grid-template-columns: 100%; | ||||
| 	overflow: clip; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { userDetailed, inviteCode } from '../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import MkInviteCode from './MkInviteCode.vue'; | ||||
| @@ -39,8 +39,8 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/show', (req, res, ctx) => { | ||||
| 					return res(ctx.json(userDetailed(req.params.userId as string))); | ||||
| 				http.post('/api/users/show', ({ params }) => { | ||||
| 					return HttpResponse.json(userDetailed(params.userId as string)); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -119,6 +119,7 @@ function close() { | ||||
| 				margin-top: 12px; | ||||
| 				font-size: 0.8em; | ||||
| 				line-height: 1.5em; | ||||
| 				text-align: center; | ||||
| 			} | ||||
|  | ||||
| 			> .indicatorWithValue { | ||||
|   | ||||
| @@ -253,7 +253,7 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti | ||||
| const isMyRenote = $i && ($i.id === note.value.userId); | ||||
| const showContent = ref(false); | ||||
| const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); | ||||
| const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null); | ||||
| const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null); | ||||
| const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); | ||||
| const collapsed = ref(appearNote.value.cw == null && isLong); | ||||
| const isDeleted = ref(false); | ||||
| @@ -385,7 +385,7 @@ function react(viaKeyboard = false): void { | ||||
| 		} | ||||
| 	} else { | ||||
| 		blur(); | ||||
| 		reactionPicker.show(reactButton.value ?? null, reaction => { | ||||
| 		reactionPicker.show(reactButton.value ?? null, note.value, reaction => { | ||||
| 			sound.playMisskeySfx('reaction'); | ||||
|  | ||||
| 			if (props.mock) { | ||||
|   | ||||
| @@ -277,7 +277,7 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals | ||||
| const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); | ||||
| const translating = ref(false); | ||||
| const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; | ||||
| const urls = parsed ? extractUrlFromMfm(parsed) : null; | ||||
| const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null; | ||||
| const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); | ||||
| const conversation = ref<Misskey.entities.Note[]>([]); | ||||
| const replies = ref<Misskey.entities.Note[]>([]); | ||||
| @@ -385,7 +385,7 @@ function react(viaKeyboard = false): void { | ||||
| 		} | ||||
| 	} else { | ||||
| 		blur(); | ||||
| 		reactionPicker.show(reactButton.value ?? null, reaction => { | ||||
| 		reactionPicker.show(reactButton.value ?? null, note.value, reaction => { | ||||
| 			sound.playMisskeySfx('reaction'); | ||||
|  | ||||
| 			misskeyApi('notes/reactions/create', { | ||||
|   | ||||
| @@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| import { onMounted, onUnmounted, ref, shallowRef } from 'vue'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import { getScrollContainer } from '@/scripts/scroll.js'; | ||||
| import { isHorizontalSwipeSwiping } from '@/scripts/touch.js'; | ||||
|  | ||||
| const SCROLL_STOP = 10; | ||||
| const MAX_PULL_DISTANCE = Infinity; | ||||
| @@ -129,7 +130,7 @@ function moveEnd() { | ||||
| function moving(event: TouchEvent | PointerEvent) { | ||||
| 	if (!isPullStart.value || isRefreshing.value || disabled) return; | ||||
|  | ||||
| 	if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) { | ||||
| 	if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) { | ||||
| 		pullDistance.value = 0; | ||||
| 		isPullEnd.value = false; | ||||
| 		moveEnd(); | ||||
| @@ -148,6 +149,10 @@ function moving(event: TouchEvent | PointerEvent) { | ||||
| 		if (event.cancelable) event.preventDefault(); | ||||
| 	} | ||||
|  | ||||
| 	if (pullDistance.value > SCROLL_STOP) { | ||||
| 		event.stopPropagation(); | ||||
| 	} | ||||
|  | ||||
| 	isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -32,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import * as sound from '@/scripts/sound.js'; | ||||
| import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; | ||||
| import { customEmojis } from '@/custom-emojis.js'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	reaction: string; | ||||
| @@ -48,13 +50,19 @@ const emit = defineEmits<{ | ||||
|  | ||||
| const buttonEl = shallowRef<HTMLElement>(); | ||||
|  | ||||
| const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); | ||||
| const isCustomEmoji = computed(() => props.reaction.includes(':')); | ||||
| const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null); | ||||
|  | ||||
| const canToggle = computed(() => { | ||||
| 	return !props.reaction.match(/@\w/) && $i | ||||
| 			&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value)) | ||||
| 			|| !isCustomEmoji.value; | ||||
| }); | ||||
| const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); | ||||
|  | ||||
| async function toggleReaction() { | ||||
| 	if (!canToggle.value) return; | ||||
|  | ||||
| 	// TODO: その絵文字を使う権限があるかどうか確認 | ||||
|  | ||||
| 	const oldReaction = props.note.myReaction; | ||||
| 	if (oldReaction) { | ||||
| 		const confirm = await os.confirm({ | ||||
| @@ -101,8 +109,8 @@ async function toggleReaction() { | ||||
| } | ||||
|  | ||||
| async function menu(ev) { | ||||
| 	if (!canToggle.value) return; | ||||
| 	if (!props.reaction.includes(':')) return; | ||||
| 	if (!canGetInfo.value) return; | ||||
|  | ||||
| 	os.popupMenu([{ | ||||
| 		text: i18n.ts.info, | ||||
| 		icon: 'ti ti-info-circle', | ||||
|   | ||||
| @@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 	</div> | ||||
| 	<div :class="$style.caption"><slot name="caption"></slot></div> | ||||
|  | ||||
| 	<MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> | ||||
| 	<MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| @@ -138,6 +138,7 @@ function show() { | ||||
| 			active: computed(() => v.value === option.props?.value), | ||||
| 			action: () => { | ||||
| 				v.value = option.props?.value; | ||||
| 				changed.value = true; | ||||
| 				emit('changeByUser', v.value); | ||||
| 			}, | ||||
| 		}); | ||||
| @@ -288,6 +289,10 @@ function show() { | ||||
| 	padding-left: 6px; | ||||
| } | ||||
|  | ||||
| .save { | ||||
| 	margin: 8px 0 0 0; | ||||
| } | ||||
|  | ||||
| .chevron { | ||||
| 	transition: transform 0.1s ease-out; | ||||
| } | ||||
|   | ||||
| @@ -18,8 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; | ||||
| import Misskey from 'misskey-js'; | ||||
| import { Connection } from 'misskey-js/built/streaming.js'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import MkNotes from '@/components/MkNotes.vue'; | ||||
| import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; | ||||
| import { useStream } from '@/stream.js'; | ||||
| @@ -87,8 +86,8 @@ function prepend(note) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| let connection: Connection; | ||||
| let connection2: Connection; | ||||
| let connection: Misskey.ChannelConnection | null = null; | ||||
| let connection2: Misskey.ChannelConnection | null = null; | ||||
| let paginationQuery: Paging | null = null; | ||||
|  | ||||
| const stream = useStream(); | ||||
| @@ -151,7 +150,7 @@ function connectChannel() { | ||||
| 			roleId: props.role, | ||||
| 		}); | ||||
| 	} | ||||
| 	if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend); | ||||
| 	if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend); | ||||
| } | ||||
|  | ||||
| function disconnectChannel() { | ||||
|   | ||||
| @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| 			v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" | ||||
| 			sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" | ||||
| 			scrolling="no" | ||||
| 			:allow="player.allow.join(';')" | ||||
| 			:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')" | ||||
| 			:class="$style.playerIframe" | ||||
| 			:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" | ||||
| 			:style="{ border: 0 }" | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import { userDetailed } from '../../.storybook/fakes.js'; | ||||
| import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue'; | ||||
| @@ -38,17 +38,17 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/users', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						userDetailed('44'), | ||||
| 						userDetailed('49'), | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 				rest.post('/api/pinned-users', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/pinned-users', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						userDetailed('44'), | ||||
| 						userDetailed('49'), | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import Misskey from 'misskey-js'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { i18n } from '@/i18n.js'; | ||||
| import MkFolder from '@/components/MkFolder.vue'; | ||||
| import XUser from '@/components/MkUserSetupDialog.User.vue'; | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { commonHandlers } from '../../.storybook/mocks.js'; | ||||
| import { userDetailed } from '../../.storybook/fakes.js'; | ||||
| import MkUserSetupDialog from './MkUserSetupDialog.vue'; | ||||
| @@ -38,17 +38,17 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/users', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						userDetailed('44'), | ||||
| 						userDetailed('49'), | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 				rest.post('/api/pinned-users', (req, res, ctx) => { | ||||
| 					return res(ctx.json([ | ||||
| 				http.post('/api/pinned-users', () => { | ||||
| 					return HttpResponse.json([ | ||||
| 						userDetailed('44'), | ||||
| 						userDetailed('49'), | ||||
| 					])); | ||||
| 					]); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
| import { expect } from '@storybook/jest'; | ||||
| import { userEvent, waitFor, within } from '@storybook/testing-library'; | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { commonHandlers } from '../../../.storybook/mocks.js'; | ||||
| import MkUrl from './MkUrl.vue'; | ||||
| export const Default = { | ||||
| @@ -59,8 +59,8 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.get('/url', (req, res, ctx) => { | ||||
| 					return res(ctx.json({ | ||||
| 				http.get('/url', () => { | ||||
| 					return HttpResponse.json({ | ||||
| 						title: 'Misskey Hub', | ||||
| 						icon: 'https://misskey-hub.net/favicon.ico', | ||||
| 						description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。', | ||||
| @@ -74,7 +74,7 @@ export const Default = { | ||||
| 						sitename: 'misskey-hub.net', | ||||
| 						sensitive: false, | ||||
| 						url: 'https://misskey-hub.net/', | ||||
| 					})); | ||||
| 					}); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| --> | ||||
|  | ||||
| <template> | ||||
| <KeepAlive :max="defaultStore.state.numberOfPageCache"> | ||||
| <KeepAlive | ||||
| 	:max="defaultStore.state.numberOfPageCache" | ||||
| 	:exclude="pageCacheController" | ||||
| > | ||||
| 	<Suspense :timeout="0"> | ||||
| 		<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> | ||||
|  | ||||
| @@ -16,9 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue'; | ||||
| import { IRouter, Resolved } from '@/nirax.js'; | ||||
| import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue'; | ||||
| import { IRouter, Resolved, RouteDef } from '@/nirax.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { globalEvents } from '@/events.js'; | ||||
| import MkLoadingPage from '@/pages/_loading_.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	router?: IRouter; | ||||
| @@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null { | ||||
| } | ||||
|  | ||||
| const current = resolveNested(router.current)!; | ||||
| const currentPageComponent = shallowRef(current.route.component); | ||||
| const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); | ||||
| const currentPageProps = ref(current.props); | ||||
| const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props))); | ||||
|  | ||||
| function onChange({ resolved, key: newKey }) { | ||||
| 	const current = resolveNested(resolved); | ||||
| 	if (current == null) return; | ||||
| 	if (current == null || 'redirect' in current.route) return; | ||||
| 	currentPageComponent.value = current.route.component; | ||||
| 	currentPageProps.value = current.props; | ||||
| 	key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props)); | ||||
|  | ||||
| 	nextTick(() => { | ||||
| 		// ページ遷移完了後に再びキャッシュを有効化 | ||||
| 		if (clearCacheRequested.value) { | ||||
| 			clearCacheRequested.value = false; | ||||
| 		} | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| router.addListener('change', onChange); | ||||
|  | ||||
| // #region キャッシュ制御 | ||||
|  | ||||
| /** | ||||
|  * キャッシュクリアが有効になったら、全キャッシュをクリアする | ||||
|  *  | ||||
|  * keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。 | ||||
|  * キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること | ||||
|  */ | ||||
| const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined); | ||||
| const clearCacheRequested = ref(false); | ||||
|  | ||||
| globalEvents.on('requestClearPageCache', () => { | ||||
| 	if (_DEV_) console.log('clear page cache requested'); | ||||
| 	if (!clearCacheRequested.value) { | ||||
| 		clearCacheRequested.value = true; | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| // #endregion | ||||
|  | ||||
| onBeforeUnmount(() => { | ||||
| 	router.removeListener('change', onChange); | ||||
| }); | ||||
|   | ||||
| @@ -4,6 +4,10 @@ | ||||
|  */ | ||||
|  | ||||
| import { EventEmitter } from 'eventemitter3'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
|  | ||||
| // TODO: 型付け | ||||
| export const globalEvents = new EventEmitter(); | ||||
| export const globalEvents = new EventEmitter<{ | ||||
| 	themeChanged: () => void; | ||||
| 	clientNotification: (notification: Misskey.entities.Notification) => void; | ||||
| 	requestClearPageCache: () => void; | ||||
| }>(); | ||||
|   | ||||
| @@ -213,13 +213,13 @@ const patronsWithIcon = [{ | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg', | ||||
| }, { | ||||
| 	name: 'taichan', | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png', | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.jpg', | ||||
| }, { | ||||
| 	name: '猫吉よりお', | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.png', | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.jpg', | ||||
| }, { | ||||
| 	name: '有栖かずみ', | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.png', | ||||
| 	icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.jpg', | ||||
| }]; | ||||
|  | ||||
| const patrons = [ | ||||
|   | ||||
| @@ -148,9 +148,9 @@ function save() { | ||||
| 		themeColor: themeColor.value === '' ? null : themeColor.value, | ||||
| 		defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value, | ||||
| 		defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value, | ||||
| 		infoImageUrl: infoImageUrl.value, | ||||
| 		notFoundImageUrl: notFoundImageUrl.value, | ||||
| 		serverErrorImageUrl: serverErrorImageUrl.value, | ||||
| 		infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value, | ||||
| 		notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value, | ||||
| 		serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value, | ||||
| 		manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)), | ||||
| 	}).then(() => { | ||||
| 		fetchInstance(); | ||||
|   | ||||
| @@ -893,7 +893,6 @@ function getGameImageDriveFile() { | ||||
| 				formData.append('file', blob); | ||||
| 				formData.append('name', `bubble-game-${Date.now()}.png`); | ||||
| 				formData.append('isSensitive', 'false'); | ||||
| 				formData.append('comment', 'null'); | ||||
| 				formData.append('i', $i.token); | ||||
| 				if (defaultStore.state.uploadFolder) { | ||||
| 					formData.append('folderId', defaultStore.state.uploadFolder); | ||||
|   | ||||
| @@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev); | ||||
| const setDefaultEmoji = () => setDefault(pinnedEmojis); | ||||
|  | ||||
| function previewReaction(ev: MouseEvent) { | ||||
| 	reactionPicker.show(getHTMLElement(ev)); | ||||
| 	reactionPicker.show(getHTMLElement(ev), null); | ||||
| } | ||||
|  | ||||
| function previewEmoji(ev: MouseEvent) { | ||||
|   | ||||
| @@ -125,6 +125,7 @@ import { langmap } from '@/scripts/langmap.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { claimAchievement } from '@/scripts/achievements.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { globalEvents } from '@/events.js'; | ||||
| import MkInfo from '@/components/MkInfo.vue'; | ||||
| import MkTextarea from '@/components/MkTextarea.vue'; | ||||
|  | ||||
| @@ -173,6 +174,7 @@ function saveFields() { | ||||
| 	os.apiWithDialog('i/update', { | ||||
| 		fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })), | ||||
| 	}); | ||||
| 	globalEvents.emit('requestClearPageCache'); | ||||
| } | ||||
|  | ||||
| function save() { | ||||
| @@ -191,6 +193,7 @@ function save() { | ||||
| 		isBot: !!profile.isBot, | ||||
| 		isCat: !!profile.isCat, | ||||
| 	}); | ||||
| 	globalEvents.emit('requestClearPageCache'); | ||||
| 	claimAchievement('profileFilled'); | ||||
| 	if (profile.name === 'syuilo' || profile.name === 'しゅいろ') { | ||||
| 		claimAchievement('setNameToSyuilo'); | ||||
| @@ -222,6 +225,7 @@ function changeAvatar(ev) { | ||||
| 		}); | ||||
| 		$i.avatarId = i.avatarId; | ||||
| 		$i.avatarUrl = i.avatarUrl; | ||||
| 		globalEvents.emit('requestClearPageCache'); | ||||
| 		claimAchievement('profileFilled'); | ||||
| 	}); | ||||
| } | ||||
| @@ -248,6 +252,7 @@ function changeBanner(ev) { | ||||
| 		}); | ||||
| 		$i.bannerId = i.bannerId; | ||||
| 		$i.bannerUrl = i.bannerUrl; | ||||
| 		globalEvents.emit('requestClearPageCache'); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js'; | ||||
| import { fetchThemes, getThemes } from '@/theme-store.js'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata.js'; | ||||
| import { miLocalStorage } from '@/local-storage.js'; | ||||
| import { unisonReload } from '@/scripts/unison-reload.js'; | ||||
| import * as os from '@/os.js'; | ||||
|  | ||||
| async function reloadAsk() { | ||||
| 	const { canceled } = await os.confirm({ | ||||
| 		type: 'info', | ||||
| 		text: i18n.ts.reloadToApplySetting, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
|  | ||||
| 	unisonReload(); | ||||
| } | ||||
|  | ||||
| const installedThemes = ref(getThemes()); | ||||
| const builtinThemes = getBuiltinThemesRef(); | ||||
| @@ -124,6 +136,7 @@ const lightThemeId = computed({ | ||||
| 		} | ||||
| 	}, | ||||
| }); | ||||
|  | ||||
| const darkMode = computed(defaultStore.makeGetterSetter('darkMode')); | ||||
| const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode')); | ||||
| const wallpaper = ref(miLocalStorage.getItem('wallpaper')); | ||||
| @@ -141,7 +154,7 @@ watch(wallpaper, () => { | ||||
| 	} else { | ||||
| 		miLocalStorage.setItem('wallpaper', wallpaper.value); | ||||
| 	} | ||||
| 	location.reload(); | ||||
| 	reloadAsk(); | ||||
| }); | ||||
|  | ||||
| onActivated(() => { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| /* eslint-disable @typescript-eslint/explicit-function-return-type */ | ||||
| import { StoryObj } from '@storybook/vue3'; | ||||
| import { rest } from 'msw'; | ||||
| import { HttpResponse, http } from 'msw'; | ||||
| import { userDetailed } from '../../../.storybook/fakes.js'; | ||||
| import { commonHandlers } from '../../../.storybook/mocks.js'; | ||||
| import home_ from './home.vue'; | ||||
| @@ -39,12 +39,13 @@ export const Default = { | ||||
| 		msw: { | ||||
| 			handlers: [ | ||||
| 				...commonHandlers, | ||||
| 				rest.post('/api/users/notes', (req, res, ctx) => { | ||||
| 					return res(ctx.json([])); | ||||
| 				http.post('/api/users/notes', () => { | ||||
| 					return HttpResponse.json([]); | ||||
| 				}), | ||||
| 				rest.get('/api/charts/user/notes', (req, res, ctx) => { | ||||
| 					const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); | ||||
| 					return res(ctx.json({ | ||||
| 				http.get('/api/charts/user/notes', ({ request }) => { | ||||
| 					const url = new URL(request.url); | ||||
| 					const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300); | ||||
| 					return HttpResponse.json({ | ||||
| 						total: Array.from({ length }, () => 0), | ||||
| 						inc: Array.from({ length }, () => 0), | ||||
| 						dec: Array.from({ length }, () => 0), | ||||
| @@ -54,11 +55,12 @@ export const Default = { | ||||
| 							renote: Array.from({ length }, () => 0), | ||||
| 							withFile: Array.from({ length }, () => 0), | ||||
| 						}, | ||||
| 					})); | ||||
| 					}); | ||||
| 				}), | ||||
| 				rest.get('/api/charts/user/pv', (req, res, ctx) => { | ||||
| 					const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300); | ||||
| 					return res(ctx.json({ | ||||
| 				http.get('/api/charts/user/pv', ({ request }) => { | ||||
| 					const url = new URL(request.url); | ||||
| 					const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300); | ||||
| 					return HttpResponse.json({ | ||||
| 						upv: { | ||||
| 							user: Array.from({ length }, () => 0), | ||||
| 							visitor: Array.from({ length }, () => 0), | ||||
| @@ -67,7 +69,7 @@ export const Default = { | ||||
| 							user: Array.from({ length }, () => 0), | ||||
| 							visitor: Array.from({ length }, () => 0), | ||||
| 						}, | ||||
| 					})); | ||||
| 					}); | ||||
| 				}), | ||||
| 			], | ||||
| 		}, | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| import { useStream } from '@/stream.js'; | ||||
| import { deepClone } from '@/scripts/clone.js'; | ||||
| import { deepMerge } from '@/scripts/merge.js'; | ||||
|  | ||||
| type StateDef = Record<string, { | ||||
| 	where: 'account' | 'device' | 'deviceAccount'; | ||||
| @@ -84,29 +85,9 @@ export class Storage<T extends StateDef> { | ||||
| 		return typeof value === 'object' && value !== null && !Array.isArray(value); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * valueにないキーをdefからもらう(再帰的)\ | ||||
| 	 * nullはそのまま、undefinedはdefの値 | ||||
| 	 **/ | ||||
| 	private mergeObject<X>(value: X, def: X): X { | ||||
| 		if (this.isPureObject(value) && this.isPureObject(def)) { | ||||
| 			const result = structuredClone(value) as X; | ||||
| 			for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { | ||||
| 				if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { | ||||
| 					result[k] = v; | ||||
| 				} else if (this.isPureObject(v) && this.isPureObject(result[k])) { | ||||
| 					const child = structuredClone(result[k]) as X[keyof X] & Record<string | number | symbol, unknown>; | ||||
| 					result[k] = this.mergeObject<typeof v>(child, v); | ||||
| 				} | ||||
| 			} | ||||
| 			return result; | ||||
| 		} | ||||
| 		return value; | ||||
| 	} | ||||
|  | ||||
| 	private mergeState<X>(value: X, def: X): X { | ||||
| 		if (this.isPureObject(value) && this.isPureObject(def)) { | ||||
| 			const merged = this.mergeObject(value, def); | ||||
| 			const merged = deepMerge(value, def); | ||||
|  | ||||
| 			if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged); | ||||
|  | ||||
| @@ -258,7 +239,7 @@ export class Storage<T extends StateDef> { | ||||
|  | ||||
| 	/** | ||||
| 	 * 特定のキーの、簡易的なgetter/setterを作ります | ||||
| 	 * 主にvue場で設定コントロールのmodelとして使う用 | ||||
| 	 * 主にvue上で設定コントロールのmodelとして使う用 | ||||
| 	 */ | ||||
| 	public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { | ||||
| 		get: () => T[K]['default']; | ||||
|   | ||||
| @@ -80,6 +80,10 @@ class MainRouterProxy implements IRouter { | ||||
| 		return this.supplier().resolve(path); | ||||
| 	} | ||||
|  | ||||
| 	init(): void { | ||||
| 		this.supplier().init(); | ||||
| 	} | ||||
|  | ||||
| 	eventNames(): Array<EventEmitter.EventNames<RouterEvent>> { | ||||
| 		return this.supplier().eventNames(); | ||||
| 	} | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| import * as Misskey from 'misskey-js'; | ||||
|  | ||||
| export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean { | ||||
|   const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? []; | ||||
|   return !(emoji.localOnly && note.user.host !== me.host) | ||||
|       && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote')) | ||||
|       && (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id))); | ||||
| } | ||||
| @@ -8,13 +8,13 @@ | ||||
| // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった | ||||
| // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 | ||||
|  | ||||
| type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[]; | ||||
| export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[]; | ||||
|  | ||||
| export function deepClone<T extends Cloneable>(x: T): T { | ||||
| 	if (typeof x === 'object') { | ||||
| 		if (x === null) return x; | ||||
| 		if (Array.isArray(x)) return x.map(deepClone) as T; | ||||
| 		const obj = {} as Record<string, Cloneable>; | ||||
| 		const obj = {} as Record<string | number | symbol, Cloneable>; | ||||
| 		for (const [k, v] of Object.entries(x)) { | ||||
| 			obj[k] = v === undefined ? undefined : deepClone(v); | ||||
| 		} | ||||
|   | ||||
| @@ -1,9 +1,51 @@ | ||||
| import { bundledThemesInfo } from 'shiki'; | ||||
| import { getHighlighterCore, loadWasm } from 'shiki/core'; | ||||
| import darkPlus from 'shiki/themes/dark-plus.mjs'; | ||||
| import type { Highlighter, LanguageRegistration } from 'shiki'; | ||||
| import { unique } from './array.js'; | ||||
| import { deepClone } from './clone.js'; | ||||
| import { deepMerge } from './merge.js'; | ||||
| import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki'; | ||||
| import { ColdDeviceStorage } from '@/store.js'; | ||||
| import lightTheme from '@/themes/_light.json5'; | ||||
| import darkTheme from '@/themes/_dark.json5'; | ||||
|  | ||||
| let _highlighter: Highlighter | null = null; | ||||
|  | ||||
| export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>; | ||||
| export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>; | ||||
| export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> { | ||||
| 	const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme')); | ||||
|  | ||||
| 	if (theme.base) { | ||||
| 		const base = [lightTheme, darkTheme].find(x => x.id === theme.base); | ||||
| 		if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter); | ||||
| 	} | ||||
| 	 | ||||
| 	if (theme.codeHighlighter) { | ||||
| 		let _res: ThemeRegistration = {}; | ||||
| 		if (theme.codeHighlighter.base === '_none_') { | ||||
| 			_res = deepClone(theme.codeHighlighter.overrides); | ||||
| 		} else { | ||||
| 			const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus; | ||||
| 			_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base); | ||||
| 		} | ||||
| 		if (_res.name == null) { | ||||
| 			_res.name = theme.id; | ||||
| 		} | ||||
| 		_res.type = mode; | ||||
|  | ||||
| 		if (getName) { | ||||
| 			return _res.name; | ||||
| 		} | ||||
| 		return _res; | ||||
| 	} | ||||
|  | ||||
| 	if (getName) { | ||||
| 		return 'dark-plus'; | ||||
| 	} | ||||
| 	return darkPlus; | ||||
| } | ||||
|  | ||||
| export async function getHighlighter(): Promise<Highlighter> { | ||||
| 	if (!_highlighter) { | ||||
| 		return await initHighlighter(); | ||||
| @@ -13,11 +55,17 @@ export async function getHighlighter(): Promise<Highlighter> { | ||||
|  | ||||
| export async function initHighlighter() { | ||||
| 	const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json'); | ||||
|  | ||||
| 	 | ||||
| 	await loadWasm(import('shiki/onig.wasm?init')); | ||||
|  | ||||
| 	// テーマの重複を消す | ||||
| 	const themes = unique([ | ||||
| 		darkPlus, | ||||
| 		...(await Promise.all([getTheme('light'), getTheme('dark')])), | ||||
| 	]); | ||||
|  | ||||
| 	const highlighter = await getHighlighterCore({ | ||||
| 		themes: [darkPlus], | ||||
| 		themes, | ||||
| 		langs: [ | ||||
| 			import('shiki/langs/javascript.mjs'), | ||||
| 			{ | ||||
| @@ -27,6 +75,20 @@ export async function initHighlighter() { | ||||
| 		], | ||||
| 	}); | ||||
|  | ||||
| 	ColdDeviceStorage.watch('lightTheme', async () => { | ||||
| 		const newTheme = await getTheme('light'); | ||||
| 		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { | ||||
| 			highlighter.loadTheme(newTheme); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	ColdDeviceStorage.watch('darkTheme', async () => { | ||||
| 		const newTheme = await getTheme('dark'); | ||||
| 		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { | ||||
| 			highlighter.loadTheme(newTheme); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	_highlighter = highlighter; | ||||
|  | ||||
| 	return highlighter; | ||||
|   | ||||
							
								
								
									
										31
									
								
								packages/frontend/src/scripts/merge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								packages/frontend/src/scripts/merge.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| /* | ||||
|  * SPDX-FileCopyrightText: syuilo and other misskey contributors | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
|  | ||||
| import { deepClone } from './clone.js'; | ||||
| import type { Cloneable } from './clone.js'; | ||||
|  | ||||
| function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> { | ||||
| 	return typeof value === 'object' && value !== null && !Array.isArray(value); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * valueにないキーをdefからもらう(再帰的)\ | ||||
|  * nullはそのまま、undefinedはdefの値 | ||||
|  **/ | ||||
| export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X { | ||||
| 	if (isPureObject(value) && isPureObject(def)) { | ||||
| 		const result = deepClone(value as Cloneable) as X; | ||||
| 		for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { | ||||
| 			if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { | ||||
| 				result[k] = v; | ||||
| 			} else if (isPureObject(v) && isPureObject(result[k])) { | ||||
| 				const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>; | ||||
| 				result[k] = deepMerge<typeof v>(child, v); | ||||
| 			} | ||||
| 		} | ||||
| 		return result; | ||||
| 	} | ||||
| 	return value; | ||||
| } | ||||
| @@ -3,6 +3,7 @@ | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
|  | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { defineAsyncComponent, Ref, ref } from 'vue'; | ||||
| import { popup } from '@/os.js'; | ||||
| import { defaultStore } from '@/store.js'; | ||||
| @@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js'; | ||||
| class ReactionPicker { | ||||
| 	private src: Ref<HTMLElement | null> = ref(null); | ||||
| 	private manualShowing = ref(false); | ||||
| 	private targetNote: Ref<Misskey.entities.Note | null> = ref(null); | ||||
| 	private onChosen?: (reaction: string) => void; | ||||
| 	private onClosed?: () => void; | ||||
|  | ||||
| @@ -23,6 +25,7 @@ class ReactionPicker { | ||||
| 			src: this.src, | ||||
| 			pinnedEmojis: reactionsRef, | ||||
| 			asReactionPicker: true, | ||||
| 			targetNote: this.targetNote, | ||||
| 			manualShowing: this.manualShowing, | ||||
| 		}, { | ||||
| 			done: reaction => { | ||||
| @@ -38,8 +41,9 @@ class ReactionPicker { | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	public show(src: HTMLElement | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { | ||||
| 	public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { | ||||
| 		this.src.value = src; | ||||
| 		this.targetNote.value = targetNote; | ||||
| 		this.manualShowing.value = true; | ||||
| 		this.onChosen = onChosen; | ||||
| 		this.onClosed = onClosed; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| import { ref } from 'vue'; | ||||
| import tinycolor from 'tinycolor2'; | ||||
| import { deepClone } from './clone.js'; | ||||
| import type { BuiltinTheme } from 'shiki'; | ||||
| import { globalEvents } from '@/events.js'; | ||||
| import lightTheme from '@/themes/_light.json5'; | ||||
| import darkTheme from '@/themes/_dark.json5'; | ||||
| @@ -18,6 +19,13 @@ export type Theme = { | ||||
| 	desc?: string; | ||||
| 	base?: 'dark' | 'light'; | ||||
| 	props: Record<string, string>; | ||||
| 	codeHighlighter?: { | ||||
| 		base: BuiltinTheme; | ||||
| 		overrides?: Record<string, any>; | ||||
| 	} | { | ||||
| 		base: '_none_'; | ||||
| 		overrides: Record<string, any>; | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
| export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); | ||||
| @@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => { | ||||
| 	return builtinThemes; | ||||
| }; | ||||
|  | ||||
| let timeout = null; | ||||
| let timeout: number | null = null; | ||||
|  | ||||
| export function applyTheme(theme: Theme, persist = true) { | ||||
| 	if (timeout) window.clearTimeout(timeout); | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|  * SPDX-License-Identifier: AGPL-3.0-only | ||||
|  */ | ||||
|  | ||||
| import { ref } from 'vue'; | ||||
| import { deviceKind } from '@/scripts/device-kind.js'; | ||||
|  | ||||
| const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; | ||||
| @@ -16,3 +17,6 @@ if (isTouchSupported && !isTouchUsing) { | ||||
| 		isTouchUsing = true; | ||||
| 	}, { passive: true }); | ||||
| } | ||||
|  | ||||
| /** (MkHorizontalSwipe) 横スワイプ中か? */ | ||||
| export const isHorizontalSwipeSwiping = ref(false); | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { miLocalStorage } from './local-storage.js'; | ||||
| import type { SoundType } from '@/scripts/sound.js'; | ||||
| import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki'; | ||||
| import { Storage } from '@/pizzax.js'; | ||||
| import { hemisphere } from '@/scripts/intl-const.js'; | ||||
|  | ||||
|   | ||||
| @@ -94,4 +94,8 @@ | ||||
| 		X16: ':alpha<0.7<@panel', | ||||
| 		X17: ':alpha<0.8<@bg', | ||||
| 	}, | ||||
|  | ||||
| 	codeHighlighter: { | ||||
| 		base: 'one-dark-pro', | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -94,4 +94,8 @@ | ||||
| 		X16: ':alpha<0.7<@panel', | ||||
| 		X17: ':alpha<0.8<@bg', | ||||
| 	}, | ||||
|  | ||||
| 	codeHighlighter: { | ||||
| 		base: 'catppuccin-latte', | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -116,6 +116,34 @@ describe('MkUrlPreview', () => { | ||||
| 		assert.strictEqual(iframe?.allow, 'fullscreen;web-share'); | ||||
| 	}); | ||||
|  | ||||
| 	test('A Summaly proxy response without allow falls back to the default', async () => { | ||||
| 		const iframe = await renderAndOpenPreview({ | ||||
| 			url: 'https://example.local', | ||||
| 			player: { | ||||
| 				url: 'https://example.local/player', | ||||
| 				width: null, | ||||
| 				height: null, | ||||
| 				allow: undefined as any, | ||||
| 			}, | ||||
| 		}); | ||||
| 		assert.exists(iframe, 'iframe should exist'); | ||||
| 		assert.strictEqual(iframe?.allow, 'autoplay;encrypted-media;fullscreen'); | ||||
| 	}); | ||||
|  | ||||
| 	test('Filtering the allow list from the Summaly proxy', async () => { | ||||
| 		const iframe = await renderAndOpenPreview({ | ||||
| 			url: 'https://example.local', | ||||
| 			player: { | ||||
| 				url: 'https://example.local/player', | ||||
| 				width: null, | ||||
| 				height: null, | ||||
| 				allow: ['autoplay', 'camera', 'fullscreen'], | ||||
| 			}, | ||||
| 		}); | ||||
| 		assert.exists(iframe, 'iframe should exist'); | ||||
| 		assert.strictEqual(iframe?.allow, 'autoplay;fullscreen'); | ||||
| 	}); | ||||
|  | ||||
| 	test('Having a player width should keep the fixed aspect ratio', async () => { | ||||
| 		const iframe = await renderAndOpenPreview({ | ||||
| 			url: 'https://example.local', | ||||
|   | ||||
| @@ -4,22 +4,6 @@ import { toPascal } from 'ts-case-convert'; | ||||
| import OpenAPIParser from '@readme/openapi-parser'; | ||||
| import openapiTS from 'openapi-typescript'; | ||||
|  | ||||
| function generateVersionHeaderComment(openApiDocs: OpenAPIV3_1.Document): string { | ||||
| 	const contents = { | ||||
| 		version: openApiDocs.info.version, | ||||
| 		generatedAt: new Date().toISOString(), | ||||
| 	}; | ||||
|  | ||||
| 	const lines: string[] = []; | ||||
| 	lines.push('/*'); | ||||
| 	for (const [key, value] of Object.entries(contents)) { | ||||
| 		lines.push(` * ${key}: ${value}`); | ||||
| 	} | ||||
| 	lines.push(' */'); | ||||
|  | ||||
| 	return lines.join('\n'); | ||||
| } | ||||
|  | ||||
| async function generateBaseTypes( | ||||
| 	openApiDocs: OpenAPIV3_1.Document, | ||||
| 	openApiJsonPath: string, | ||||
| @@ -36,9 +20,6 @@ async function generateBaseTypes( | ||||
| 	} | ||||
| 	lines.push(''); | ||||
|  | ||||
| 	lines.push(generateVersionHeaderComment(openApiDocs)); | ||||
| 	lines.push(''); | ||||
|  | ||||
| 	const generatedTypes = await openapiTS(openApiJsonPath, { exportType: true }); | ||||
| 	lines.push(generatedTypes); | ||||
| 	lines.push(''); | ||||
| @@ -59,8 +40,6 @@ async function generateSchemaEntities( | ||||
| 	const schemaNames = Object.keys(schemas); | ||||
| 	const typeAliasLines: string[] = []; | ||||
|  | ||||
| 	typeAliasLines.push(generateVersionHeaderComment(openApiDocs)); | ||||
| 	typeAliasLines.push(''); | ||||
| 	typeAliasLines.push(`import { components } from '${toImportPath(typeFileName)}';`); | ||||
| 	typeAliasLines.push( | ||||
| 		...schemaNames.map(it => `export type ${it} = components['schemas']['${it}'];`), | ||||
| @@ -119,9 +98,6 @@ async function generateEndpoints( | ||||
|  | ||||
| 	const entitiesOutputLine: string[] = []; | ||||
|  | ||||
| 	entitiesOutputLine.push(generateVersionHeaderComment(openApiDocs)); | ||||
| 	entitiesOutputLine.push(''); | ||||
|  | ||||
| 	entitiesOutputLine.push(`import { operations } from '${toImportPath(typeFileName)}';`); | ||||
| 	entitiesOutputLine.push(''); | ||||
|  | ||||
| @@ -139,9 +115,6 @@ async function generateEndpoints( | ||||
|  | ||||
| 	const endpointOutputLine: string[] = []; | ||||
|  | ||||
| 	endpointOutputLine.push(generateVersionHeaderComment(openApiDocs)); | ||||
| 	endpointOutputLine.push(''); | ||||
|  | ||||
| 	endpointOutputLine.push('import type {'); | ||||
| 	endpointOutputLine.push( | ||||
| 		...[emptyRequest, emptyResponse, ...entities].map(it => '\t' + it.generateName() + ','), | ||||
| @@ -187,9 +160,6 @@ async function generateApiClientJSDoc( | ||||
|  | ||||
| 	const endpointOutputLine: string[] = []; | ||||
|  | ||||
| 	endpointOutputLine.push(generateVersionHeaderComment(openApiDocs)); | ||||
| 	endpointOutputLine.push(''); | ||||
|  | ||||
| 	endpointOutputLine.push(`import type { SwitchCaseResponseType } from '${toImportPath(apiClientFileName)}';`); | ||||
| 	endpointOutputLine.push(`import type { Endpoints } from '${toImportPath(endpointsFileName)}';`); | ||||
| 	endpointOutputLine.push(''); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
| 	"type": "module", | ||||
| 	"name": "misskey-js", | ||||
| 	"version": "2024.2.0-beta.8", | ||||
| 	"version": "2024.2.0-beta.10", | ||||
| 	"description": "Misskey SDK for JavaScript", | ||||
| 	"types": "./built/dts/index.d.ts", | ||||
| 	"exports": { | ||||
|   | ||||
| @@ -1,8 +1,3 @@ | ||||
| /* | ||||
|  * version: 2024.2.0-beta.9 | ||||
|  * generatedAt: 2024-02-05T02:03:49.797Z | ||||
|  */ | ||||
|  | ||||
| import type { SwitchCaseResponseType } from '../api.js'; | ||||
| import type { Endpoints } from './endpoint.js'; | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,3 @@ | ||||
| /* | ||||
|  * version: 2024.2.0-beta.9 | ||||
|  * generatedAt: 2024-02-05T02:03:49.795Z | ||||
|  */ | ||||
|  | ||||
| import type { | ||||
| 	EmptyRequest, | ||||
| 	EmptyResponse, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo