From 7ec07d5fd2e28d523c85d160944916b2a5ee04a1 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sat, 8 Jul 2023 21:18:16 +0900
Subject: [PATCH 01/17] perf(backend): Reduce memory usage of MemoryKVCache
 (#11076)

* perf(backend): Reduce memory usage of MemoryKVCache

* fix
---
 packages/backend/src/core/CacheService.ts | 47 +++++++++++++++++++----
 packages/backend/src/misc/cache.ts        | 36 ++++++++++++-----
 2 files changed, 65 insertions(+), 18 deletions(-)

diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts
index 2b7f9a48da..cd6b68e721 100644
--- a/packages/backend/src/core/CacheService.ts
+++ b/packages/backend/src/core/CacheService.ts
@@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
 
 @Injectable()
 export class CacheService implements OnApplicationShutdown {
-	public userByIdCache: MemoryKVCache<User>;
-	public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
+	public userByIdCache: MemoryKVCache<User, User | string>;
+	public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null, string | null>;
 	public localUserByIdCache: MemoryKVCache<LocalUser>;
-	public uriPersonCache: MemoryKVCache<User | null>;
+	public uriPersonCache: MemoryKVCache<User | null, string | null>;
 	public userProfileCache: RedisKVCache<UserProfile>;
 	public userMutingsCache: RedisKVCache<Set<string>>;
 	public userBlockingCache: RedisKVCache<Set<string>>;
@@ -55,10 +55,41 @@ export class CacheService implements OnApplicationShutdown {
 	) {
 		//this.onMessage = this.onMessage.bind(this);
 
-		this.userByIdCache = new MemoryKVCache<User>(Infinity);
-		this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
-		this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
-		this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
+		const localUserByIdCache = new MemoryKVCache<LocalUser>(1000 * 60 * 60 * 6 /* 6h */);
+		this.localUserByIdCache	= localUserByIdCache;
+
+		// ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する
+		const userByIdCache = new MemoryKVCache<User, User | string>(1000 * 60 * 60 * 6 /* 6h */, {
+			toMapConverter: user => {
+				if (user.host === null) {
+					localUserByIdCache.set(user.id, user as LocalUser);
+					return user.id;
+				}
+
+				return user;
+			},
+			fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId,
+		});
+		this.userByIdCache = userByIdCache;
+
+		this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null, string | null>(Infinity, {
+			toMapConverter: user => {
+				if (user === null) return null;
+
+				localUserByIdCache.set(user.id, user);
+				return user.id;
+			},
+			fromMapConverter: id => id === null ? null : localUserByIdCache.get(id),
+		});
+		this.uriPersonCache = new MemoryKVCache<User | null, string | null>(Infinity, {
+			toMapConverter: user => {
+				if (user === null) return null;
+
+				userByIdCache.set(user.id, user);
+				return user.id;
+			},
+			fromMapConverter: id => id === null ? null : userByIdCache.get(id),
+		});
 
 		this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
 			lifetime: 1000 * 60 * 30, // 30m
@@ -131,7 +162,7 @@ export class CacheService implements OnApplicationShutdown {
 					const user = await this.usersRepository.findOneByOrFail({ id: body.id });
 					this.userByIdCache.set(user.id, user);
 					for (const [k, v] of this.uriPersonCache.cache.entries()) {
-						if (v.value?.id === user.id) {
+						if (v.value === user.id) {
 							this.uriPersonCache.set(k, user);
 						}
 					}
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index f130a7db8b..e825d51371 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -181,14 +181,28 @@ export class RedisSingleCache<T> {
 
 // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
 
-export class MemoryKVCache<T> {
-	public cache: Map<string, { date: number; value: T; }>;
+function nothingToDo<T, V = T>(value: T): V {
+	return value as unknown as V;
+}
+
+export class MemoryKVCache<T, V = T> {
+	public cache: Map<string, { date: number; value: V; }>;
 	private lifetime: number;
 	private gcIntervalHandle: NodeJS.Timer;
-
-	constructor(lifetime: MemoryKVCache<never>['lifetime']) {
+	private toMapConverter: (value: T) => V;
+	private fromMapConverter: (cached: V) => T | undefined;
+
+	constructor(lifetime: MemoryKVCache<never>['lifetime'], options: {
+		toMapConverter: (value: T) => V;
+		fromMapConverter: (cached: V) => T | undefined;
+	} = {
+		toMapConverter: nothingToDo,
+		fromMapConverter: nothingToDo,
+	}) {
 		this.cache = new Map();
 		this.lifetime = lifetime;
+		this.toMapConverter = options.toMapConverter;
+		this.fromMapConverter = options.fromMapConverter;
 
 		this.gcIntervalHandle = setInterval(() => {
 			this.gc();
@@ -199,7 +213,7 @@ export class MemoryKVCache<T> {
 	public set(key: string, value: T): void {
 		this.cache.set(key, {
 			date: Date.now(),
-			value,
+			value: this.toMapConverter(value),
 		});
 	}
 
@@ -211,7 +225,7 @@ export class MemoryKVCache<T> {
 			this.cache.delete(key);
 			return undefined;
 		}
-		return cached.value;
+		return this.fromMapConverter(cached.value);
 	}
 
 	@bindThis
@@ -222,9 +236,10 @@ export class MemoryKVCache<T> {
 	/**
 	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 	 * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
+	 * fetcherの引数はcacheに保存されている値があれば渡されます
 	 */
 	@bindThis
-	public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
+	public async fetch(key: string, fetcher: (value: V | undefined) => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
 		const cachedValue = this.get(key);
 		if (cachedValue !== undefined) {
 			if (validator) {
@@ -239,7 +254,7 @@ export class MemoryKVCache<T> {
 		}
 
 		// Cache MISS
-		const value = await fetcher();
+		const value = await fetcher(this.cache.get(key)?.value);
 		this.set(key, value);
 		return value;
 	}
@@ -247,9 +262,10 @@ export class MemoryKVCache<T> {
 	/**
 	 * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
 	 * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
+	 * fetcherの引数はcacheに保存されている値があれば渡されます
 	 */
 	@bindThis
-	public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
+	public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
 		const cachedValue = this.get(key);
 		if (cachedValue !== undefined) {
 			if (validator) {
@@ -264,7 +280,7 @@ export class MemoryKVCache<T> {
 		}
 
 		// Cache MISS
-		const value = await fetcher();
+		const value = await fetcher(this.cache.get(key)?.value);
 		if (value !== undefined) {
 			this.set(key, value);
 		}
-- 
GitLab


From 60366a45585654115d5178486cc747cf0ee9450a Mon Sep 17 00:00:00 2001
From: Caipira <caipira@libnare.net>
Date: Sat, 8 Jul 2023 21:31:38 +0900
Subject: [PATCH 02/17] fix(backend): Remove Meilisearch index when notes are
 deleted (#10988)

* fix(backend): Include feature to delete Meilisearch index notes

* Update variable name
`cascadingNotesFilter` -> `federatedLocalCascadingNotes`

* tweak

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 packages/backend/src/core/NoteDeleteService.ts     | 14 +++++++++++---
 packages/backend/src/core/SearchService.ts         |  9 +++++++++
 .../processors/DeleteAccountProcessorService.ts    |  6 ++++++
 3 files changed, 26 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index 10381c7e65..f77ea8aab4 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { MetaService } from '@/core/MetaService.js';
+import { SearchService } from '@/core/SearchService.js';
 
 @Injectable()
 export class NoteDeleteService {
@@ -41,6 +42,7 @@ export class NoteDeleteService {
 		private apRendererService: ApRendererService,
 		private apDeliverManagerService: ApDeliverManagerService,
 		private metaService: MetaService,
+		private searchService: SearchService,
 		private notesChart: NotesChart,
 		private perUserNotesChart: PerUserNotesChart,
 		private instanceChart: InstanceChart,
@@ -53,6 +55,7 @@ export class NoteDeleteService {
 	 */
 	async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) {
 		const deletedAt = new Date();
+		const cascadingNotes = await this.findCascadingNotes(note);
 
 		// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
 		if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
@@ -88,8 +91,8 @@ export class NoteDeleteService {
 			}
 
 			// also deliever delete activity to cascaded notes
-			const cascadingNotes = (await this.findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes
-			for (const cascadingNote of cascadingNotes) {
+			const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
+			for (const cascadingNote of federatedLocalCascadingNotes) {
 				if (!cascadingNote.user) continue;
 				if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
 				const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
@@ -114,6 +117,11 @@ export class NoteDeleteService {
 			}
 		}
 
+		for (const cascadingNote of cascadingNotes) {
+			this.searchService.unindexNote(cascadingNote);
+		}
+		this.searchService.unindexNote(note);
+
 		await this.notesRepository.delete({
 			id: note.id,
 			userId: user.id,
@@ -140,7 +148,7 @@ export class NoteDeleteService {
 
 		const cascadingNotes: Note[] = await recursive(note.id);
 
-		return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
+		return cascadingNotes;
 	}
 
 	@bindThis
diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts
index 956c4cc09c..28b8ee8073 100644
--- a/packages/backend/src/core/SearchService.ts
+++ b/packages/backend/src/core/SearchService.ts
@@ -115,6 +115,15 @@ export class SearchService {
 		}
 	}
 
+	@bindThis
+	public async unindexNote(note: Note): Promise<void> {
+		if (!['home', 'public'].includes(note.visibility)) return;
+
+		if (this.meilisearch) {
+			this.meilisearchNoteIndex!.deleteDocument(note.id);
+		}
+	}
+
 	@bindThis
 	public async searchNote(q: string, me: User | null, opts: {
 		userId?: Note['userId'] | null;
diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
index 39dd801af0..65ded170b7 100644
--- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts
@@ -12,6 +12,7 @@ import { bindThis } from '@/decorators.js';
 import { QueueLoggerService } from '../QueueLoggerService.js';
 import type * as Bull from 'bullmq';
 import type { DbUserDeleteJobData } from '../types.js';
+import { SearchService } from "@/core/SearchService.js";
 
 @Injectable()
 export class DeleteAccountProcessorService {
@@ -36,6 +37,7 @@ export class DeleteAccountProcessorService {
 		private driveService: DriveService,
 		private emailService: EmailService,
 		private queueLoggerService: QueueLoggerService,
+		private searchService: SearchService,
 	) {
 		this.logger = this.queueLoggerService.logger.createSubLogger('delete-account');
 	}
@@ -71,6 +73,10 @@ export class DeleteAccountProcessorService {
 				cursor = notes[notes.length - 1].id;
 
 				await this.notesRepository.delete(notes.map(note => note.id));
+
+				for (const note of notes) {
+					await this.searchService.unindexNote(note);
+				}
 			}
 
 			this.logger.succ('All of notes deleted');
-- 
GitLab


From 74a05ec7396ba6f07a8dd3bff77ed0b38360d374 Mon Sep 17 00:00:00 2001
From: Kagami Sascha Rosylight <saschanaz@outlook.com>
Date: Sun, 9 Jul 2023 00:06:13 +0200
Subject: [PATCH 03/17] fix(frontend): fix storybook build (#11203)

---
 packages/frontend/.storybook/generate.tsx | 32 ++++++++++++-----------
 1 file changed, 17 insertions(+), 15 deletions(-)

diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index f442422109..b3d7bd8f5e 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -96,7 +96,7 @@ declare global {
 	}
 }
 
-function toStories(component: string): string {
+function toStories(component: string): Promise<string> {
 	const msw = `${component.slice(0, -'.vue'.length)}.msw`;
 	const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`;
 	const metaStories = `${component.slice(0, -'.vue'.length)}.stories.meta`;
@@ -394,18 +394,20 @@ function toStories(component: string): string {
 }
 
 // glob('src/{components,pages,ui,widgets}/**/*.vue')
-Promise.all([
-	glob('src/components/global/*.vue'),
-	glob('src/components/Mk{A,B}*.vue'),
-	glob('src/components/MkDigitalClock.vue'),
-	glob('src/components/MkGalleryPostPreview.vue'),
-	glob('src/components/MkSignupServerRules.vue'),
-	glob('src/components/MkUserSetupDialog.vue'),
-	glob('src/components/MkUserSetupDialog.*.vue'),
-	glob('src/pages/user/home.vue'),
-])
-	.then((globs) => globs.flat())
-	.then((components) => Promise.all(components.map((component) => {
+(async () => {
+	const globs = await Promise.all([
+		glob('src/components/global/*.vue'),
+		glob('src/components/Mk{A,B}*.vue'),
+		glob('src/components/MkDigitalClock.vue'),
+		glob('src/components/MkGalleryPostPreview.vue'),
+		glob('src/components/MkSignupServerRules.vue'),
+		glob('src/components/MkUserSetupDialog.vue'),
+		glob('src/components/MkUserSetupDialog.*.vue'),
+		glob('src/pages/user/home.vue'),
+	]);
+	const components = globs.flat();
+	await Promise.all(components.map(async (component) => {
 		const stories = component.replace(/\.vue$/, '.stories.ts');
-		return writeFile(stories, toStories(component));
-	})));
+		await writeFile(stories, await toStories(component));
+	}))
+})();
-- 
GitLab


From 5059d4d7e1fc98c5a86ebe73997e21b8c9bda0f7 Mon Sep 17 00:00:00 2001
From: Kagami Sascha Rosylight <saschanaz@outlook.com>
Date: Sun, 9 Jul 2023 01:59:44 +0200
Subject: [PATCH 04/17] refactor(backend): skip fetching notes when the data is
 same-origin (#11200)

* refactor(backend): skip fetching notes when the data is same-origin

* Update CHANGELOG.md

* sentFrom
---
 CHANGELOG.md                                  |   1 +
 .../core/activitypub/models/ApNoteService.ts  |  10 +-
 .../activitypub/models/ApPersonService.ts     |  11 +-
 packages/backend/test/misc/mock-resolver.ts   |  19 ++-
 packages/backend/test/unit/activitypub.ts     | 113 ++++++++++++++++--
 5 files changed, 131 insertions(+), 23 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35162f6b0f..c28d0b9bfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,7 @@
 - nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
 - 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
 - 全体的なDBクエリのパフォーマンスを向上
+- featuredノートのsignedGet回数を減らしました
 
 ## 13.13.2
 
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 35865a819d..d3359ef900 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -177,7 +177,7 @@ export class ApNoteService {
 
 		// リプライ
 		const reply: Note | null = note.inReplyTo
-			? await this.resolveNote(note.inReplyTo, resolver)
+			? await this.resolveNote(note.inReplyTo, { resolver })
 				.then(x => {
 					if (x == null) {
 						this.logger.warn('Specified inReplyTo, but not found');
@@ -293,9 +293,8 @@ export class ApNoteService {
 	 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
 	 */
 	@bindThis
-	public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
-		const uri = typeof value === 'string' ? value : value.id;
-		if (uri == null) throw new Error('missing uri');
+	public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<Note | null> {
+		const uri = getApId(value);
 
 		// ブロックしていたら中断
 		const meta = await this.metaService.fetch();
@@ -318,7 +317,8 @@ export class ApNoteService {
 			// リモートサーバーからフェッチしてきて登録
 			// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
 			// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
-			return await this.createNote(uri, resolver, true);
+			const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
+			return await this.createNote(createFrom, options.resolver, true);
 		} finally {
 			unlock();
 		}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index e8b65b3d42..e89ee4632c 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -260,7 +260,7 @@ export class ApPersonService implements OnModuleInit {
 		// Create user
 		let user: RemoteUser | null = null;
 		try {
-		// Start transaction
+			// Start transaction
 			await this.db.transaction(async transactionalEntityManager => {
 				user = await transactionalEntityManager.save(new User({
 					id: this.idService.genId(),
@@ -306,9 +306,9 @@ export class ApPersonService implements OnModuleInit {
 				}
 			});
 		} catch (e) {
-		// duplicate key error
+			// duplicate key error
 			if (isDuplicateKeyValueError(e)) {
-			// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
+				// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
 				const u = await this.usersRepository.findOneBy({ uri: person.id });
 				if (u == null) throw new Error('already registered');
 
@@ -604,7 +604,10 @@ export class ApPersonService implements OnModuleInit {
 		const featuredNotes = await Promise.all(items
 			.filter(item => getApType(item) === 'Note')	// TODO: Noteでなくてもいいかも
 			.slice(0, 5)
-			.map(item => limit(() => this.apNoteService.resolveNote(item, _resolver))));
+			.map(item => limit(() => this.apNoteService.resolveNote(item, {
+				resolver: _resolver,
+				sentFrom: new URL(user.uri),
+			}))));
 
 		await this.db.transaction(async transactionalEntityManager => {
 			await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts
index a7bcd859ae..9dbe77a7c4 100644
--- a/packages/backend/test/misc/mock-resolver.ts
+++ b/packages/backend/test/misc/mock-resolver.ts
@@ -18,7 +18,8 @@ type MockResponse = {
 };
 
 export class MockResolver extends Resolver {
-	private _rs = new Map<string, MockResponse>();
+	#responseMap = new Map<string, MockResponse>();
+	#remoteGetTrials: string[] = [];
 
 	constructor(loggerService: LoggerService) {
 		super(
@@ -38,18 +39,28 @@ export class MockResolver extends Resolver {
 		);
 	}
 
-	public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') {
-		this._rs.set(uri, {
+	public register(uri: string, content: string | Record<string, any>, type = 'application/activity+json'): void {
+		this.#responseMap.set(uri, {
 			type,
 			content: typeof content === 'string' ? content : JSON.stringify(content),
 		});
 	}
 
+	public clear(): void {
+		this.#responseMap.clear();
+		this.#remoteGetTrials.length = 0;
+	}
+
+	public remoteGetTrials(): string[] {
+		return this.#remoteGetTrials;
+	}
+
 	@bindThis
 	public async resolve(value: string | IObject): Promise<IObject> {
 		if (typeof value !== 'string') return value;
 
-		const r = this._rs.get(value);
+		this.#remoteGetTrials.push(value);
+		const r = this.#responseMap.get(value);
 
 		if (!r) {
 			throw new Error('Not registed for mock');
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 7cd740a2fa..02b900da9b 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -11,16 +11,19 @@ import { GlobalModule } from '@/GlobalModule.js';
 import { CoreModule } from '@/core/CoreModule.js';
 import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
 import { LoggerService } from '@/core/LoggerService.js';
-import type { IActor } from '@/core/activitypub/type.js';
+import type { IActor, ICollection, IPost } from '@/core/activitypub/type.js';
 import { Note } from '@/models/index.js';
 import { secureRndstr } from '@/misc/secure-rndstr.js';
 import { MockResolver } from '../misc/mock-resolver.js';
 
 const host = 'https://host1.test';
 
-function createRandomActor(): IActor & { id: string } {
+type NonTransientIActor = IActor & { id: string };
+type NonTransientIPost = IPost & { id: string };
+
+function createRandomActor({ actorHost = host } = {}): NonTransientIActor {
 	const preferredUsername = secureRndstr(8);
-	const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
+	const actorId = `${actorHost}/users/${preferredUsername.toLowerCase()}`;
 
 	return {
 		'@context': 'https://www.w3.org/ns/activitystreams',
@@ -32,13 +35,41 @@ function createRandomActor(): IActor & { id: string } {
 	};
 }
 
+function createRandomNote(actor: NonTransientIActor): NonTransientIPost {
+	const id = secureRndstr(8);
+	const noteId = `${new URL(actor.id).origin}/notes/${id}`;
+
+	return {
+		id: noteId,
+		type: 'Note',
+		attributedTo: actor.id,
+		content: 'test test foo',
+	};
+}
+
+function createRandomNotes(actor: NonTransientIActor, length: number): NonTransientIPost[] {
+	return new Array(length).fill(null).map(() => createRandomNote(actor));
+}
+
+function createRandomFeaturedCollection(actor: NonTransientIActor, length: number): ICollection {
+	const items = createRandomNotes(actor, length);
+
+	return {
+		'@context': 'https://www.w3.org/ns/activitystreams',
+		type: 'Collection',
+		id: actor.outbox as string,
+		totalItems: items.length,
+		items,
+	};
+}
+
 describe('ActivityPub', () => {
 	let noteService: ApNoteService;
 	let personService: ApPersonService;
 	let rendererService: ApRendererService;
 	let resolver: MockResolver;
 
-	beforeEach(async () => {
+	beforeAll(async () => {
 		const app = await Test.createTestingModule({
 			imports: [GlobalModule, CoreModule],
 		}).compile();
@@ -53,7 +84,11 @@ describe('ActivityPub', () => {
 
 		// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
 		const federatedInstanceService = app.get<FederatedInstanceService>(FederatedInstanceService);
-		jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => {}));
+		jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { }));
+	});
+
+	beforeEach(() => {
+		resolver.clear();
 	});
 
 	describe('Parse minimum object', () => {
@@ -69,7 +104,7 @@ describe('ActivityPub', () => {
 		};
 
 		test('Minimum Actor', async () => {
-			resolver._register(actor.id, actor);
+			resolver.register(actor.id, actor);
 
 			const user = await personService.createPerson(actor.id, resolver);
 
@@ -79,8 +114,8 @@ describe('ActivityPub', () => {
 		});
 
 		test('Minimum Note', async () => {
-			resolver._register(actor.id, actor);
-			resolver._register(post.id, post);
+			resolver.register(actor.id, actor);
+			resolver.register(post.id, post);
 
 			const note = await noteService.createNote(post.id, resolver, true);
 
@@ -97,7 +132,7 @@ describe('ActivityPub', () => {
 				name: secureRndstr(129),
 			};
 
-			resolver._register(actor.id, actor);
+			resolver.register(actor.id, actor);
 
 			const user = await personService.createPerson(actor.id, resolver);
 
@@ -110,7 +145,7 @@ describe('ActivityPub', () => {
 				name: '',
 			};
 
-			resolver._register(actor.id, actor);
+			resolver.register(actor.id, actor);
 
 			const user = await personService.createPerson(actor.id, resolver);
 
@@ -126,4 +161,62 @@ describe('ActivityPub', () => {
 			} as Note);
 		});
 	});
+
+	describe('Featured', () => {
+		test('Fetch featured notes from IActor', async () => {
+			const actor = createRandomActor();
+			actor.featured = `${actor.id}/collections/featured`;
+
+			const featured = createRandomFeaturedCollection(actor, 5);
+
+			resolver.register(actor.id, actor);
+			resolver.register(actor.featured, featured);
+
+			await personService.createPerson(actor.id, resolver);
+
+			// All notes in `featured` are same-origin, no need to fetch notes again
+			assert.deepStrictEqual(resolver.remoteGetTrials(), [actor.id, actor.featured]);
+
+			// Created notes without resolving anything
+			for (const item of featured.items as IPost[]) {
+				const note = await noteService.fetchNote(item);
+				assert.ok(note);
+				assert.strictEqual(note.text, 'test test foo');
+				assert.strictEqual(note.uri, item.id);
+			}
+		});
+
+		test('Fetch featured notes from IActor pointing to another remote server', async () => {
+			const actor1 = createRandomActor();
+			actor1.featured = `${actor1.id}/collections/featured`;
+			const actor2 = createRandomActor({ actorHost: 'https://host2.test' });
+
+			const actor2Note = createRandomNote(actor2);
+			const featured = createRandomFeaturedCollection(actor1, 0);
+			(featured.items as IPost[]).push({
+				...actor2Note,
+				content: 'test test bar', // fraud!
+			});
+
+			resolver.register(actor1.id, actor1);
+			resolver.register(actor1.featured, featured);
+			resolver.register(actor2.id, actor2);
+			resolver.register(actor2Note.id, actor2Note);
+
+			await personService.createPerson(actor1.id, resolver);
+
+			// actor2Note is from a different server and needs to be fetched again
+			assert.deepStrictEqual(
+				resolver.remoteGetTrials(),
+				[actor1.id, actor1.featured, actor2Note.id, actor2.id],
+			);
+
+			const note = await noteService.fetchNote(actor2Note.id);
+			assert.ok(note);
+
+			// Reflects the original content instead of the fraud
+			assert.strictEqual(note.text, 'test test foo');
+			assert.strictEqual(note.uri, actor2Note.id);
+		});
+	});
 });
-- 
GitLab


From 59046d583dc1ef216760b83012ef9d876407ae38 Mon Sep 17 00:00:00 2001
From: Kagami Sascha Rosylight <saschanaz@outlook.com>
Date: Sun, 9 Jul 2023 10:19:07 +0200
Subject: [PATCH 05/17] refactor(locales, sw): use es module (#11204)

* refactor(locales): use es module

* fix sw build

* fix gulp

* try fixing storybook

* Revert "try fixing storybook"

This reverts commit 5f2a4eee016776381a7d80407e28d129c252228f.

* try fixing storybook 2

* Update main.ts

* Update build.js

* Update main.ts

* Update changes.ts

* fix sw lint

* Update build.js
---
 gulpfile.js => gulpfile.mjs                    | 14 +++++++-------
 locales/generateDTS.js                         |  8 ++++----
 locales/index.js                               |  8 ++++----
 locales/package.json                           |  3 +++
 packages/frontend/.storybook/changes.ts        |  9 ++++++---
 packages/frontend/.storybook/main.ts           |  9 +++++++--
 packages/frontend/.storybook/package.json      |  3 +++
 packages/frontend/.storybook/preload-locale.ts |  7 +++----
 packages/frontend/.storybook/preload-theme.ts  |  7 +++----
 packages/frontend/.storybook/preview.ts        |  8 ++++----
 packages/frontend/.storybook/tsconfig.json     |  2 ++
 packages/sw/{.eslintrc.js => .eslintrc.cjs}    |  0
 packages/sw/build.js                           |  9 ++++++---
 packages/sw/package.json                       |  3 ++-
 14 files changed, 54 insertions(+), 36 deletions(-)
 rename gulpfile.js => gulpfile.mjs (88%)
 create mode 100644 locales/package.json
 create mode 100644 packages/frontend/.storybook/package.json
 rename packages/sw/{.eslintrc.js => .eslintrc.cjs} (100%)

diff --git a/gulpfile.js b/gulpfile.mjs
similarity index 88%
rename from gulpfile.js
rename to gulpfile.mjs
index 6507aad60e..9556eb795f 100644
--- a/gulpfile.js
+++ b/gulpfile.mjs
@@ -2,14 +2,14 @@
  * Gulp tasks
  */
 
-const fs = require('fs');
-const gulp = require('gulp');
-const replace = require('gulp-replace');
-const terser = require('gulp-terser');
-const cssnano = require('gulp-cssnano');
+import * as fs from 'node:fs';
+import gulp from 'gulp';
+import replace from 'gulp-replace';
+import terser from 'gulp-terser';
+import cssnano from 'gulp-cssnano';
 
-const locales = require('./locales');
-const meta = require('./package.json');
+import locales from './locales/index.js';
+import meta from './package.json' assert { type: "json" };
 
 gulp.task('copy:backend:views', () =>
 	gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
diff --git a/locales/generateDTS.js b/locales/generateDTS.js
index 5949aee7cd..bc98276325 100644
--- a/locales/generateDTS.js
+++ b/locales/generateDTS.js
@@ -1,6 +1,6 @@
-const fs = require('fs');
-const yaml = require('js-yaml');
-const ts = require('typescript');
+import * as fs from 'node:fs';
+import * as yaml from 'js-yaml';
+import * as ts from 'typescript';
 
 function createMembers(record) {
 	return Object.entries(record)
@@ -14,7 +14,7 @@ function createMembers(record) {
 		));
 }
 
-module.exports = function generateDTS() {
+export default function generateDTS() {
 	const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
 	const members = createMembers(locale);
 	const elements = [
diff --git a/locales/index.js b/locales/index.js
index 2248bb6ac9..7801f1275b 100644
--- a/locales/index.js
+++ b/locales/index.js
@@ -2,8 +2,8 @@
  * Languages Loader
  */
 
-const fs = require('fs');
-const yaml = require('js-yaml');
+import * as fs from 'node:fs';
+import * as yaml from 'js-yaml';
 
 const merge = (...args) => args.reduce((a, c) => ({
 	...a,
@@ -51,9 +51,9 @@ const primaries = {
 // 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
 const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
 
-const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
+const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
 
-module.exports = Object.entries(locales)
+export default Object.entries(locales)
 	.reduce((a, [k ,v]) => (a[k] = (() => {
 		const [lang] = k.split('-');
 		switch (k) {
diff --git a/locales/package.json b/locales/package.json
new file mode 100644
index 0000000000..bedb411a91
--- /dev/null
+++ b/locales/package.json
@@ -0,0 +1,3 @@
+{
+	"type": "module"
+}
diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts
index fc0f0c286b..a1275132be 100644
--- a/packages/frontend/.storybook/changes.ts
+++ b/packages/frontend/.storybook/changes.ts
@@ -1,7 +1,10 @@
 import fs from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
 import path from 'node:path';
 import micromatch from 'micromatch';
-import main from './main';
+import main from './main.js';
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url));
 
 interface Stats {
 	readonly modules: readonly {
@@ -13,8 +16,8 @@ interface Stats {
 	}[];
 }
 
-fs.readFile(
-	path.resolve(__dirname, '../storybook-static/preview-stats.json')
+await fs.readFile(
+	new URL('../storybook-static/preview-stats.json', import.meta.url)
 ).then((buffer) => {
 	const stats: Stats = JSON.parse(buffer.toString());
 	const keys = new Set(stats.modules.map((stat) => stat.id));
diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts
index 1d0ce5ab63..b64979980a 100644
--- a/packages/frontend/.storybook/main.ts
+++ b/packages/frontend/.storybook/main.ts
@@ -1,7 +1,11 @@
 import { resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
 import type { StorybookConfig } from '@storybook/vue3-vite';
 import { type Plugin, mergeConfig } from 'vite';
 import turbosnap from 'vite-plugin-turbosnap';
+
+const dirname = fileURLToPath(new URL('.', import.meta.url));
+
 const config = {
 	stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
 	addons: [
@@ -9,7 +13,7 @@ const config = {
 		'@storybook/addon-interactions',
 		'@storybook/addon-links',
 		'@storybook/addon-storysource',
-		resolve(__dirname, '../node_modules/storybook-addon-misskey-theme'),
+		resolve(dirname, '../node_modules/storybook-addon-misskey-theme'),
 	],
 	framework: {
 		name: '@storybook/vue3-vite',
@@ -28,7 +32,8 @@ const config = {
 		}
 		return mergeConfig(config, {
 			plugins: [
-				turbosnap({
+				// XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8
+				(turbosnap as any as typeof turbosnap['default'])({
 					rootDir: config.root ?? process.cwd(),
 				}),
 			],
diff --git a/packages/frontend/.storybook/package.json b/packages/frontend/.storybook/package.json
new file mode 100644
index 0000000000..bedb411a91
--- /dev/null
+++ b/packages/frontend/.storybook/package.json
@@ -0,0 +1,3 @@
+{
+	"type": "module"
+}
diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts
index a54164742a..636931967f 100644
--- a/packages/frontend/.storybook/preload-locale.ts
+++ b/packages/frontend/.storybook/preload-locale.ts
@@ -1,9 +1,8 @@
 import { writeFile } from 'node:fs/promises';
-import { resolve } from 'node:path';
-import * as locales from '../../../locales';
+import * as locales from '../../../locales/index.js';
 
-writeFile(
-	resolve(__dirname, 'locale.ts'),
+await writeFile(
+	new URL('locale.ts', import.meta.url),
 	`export default ${JSON.stringify(locales['ja-JP'], undefined, 2)} as const;`,
 	'utf8',
 )
diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts
index 1ff8f71ecd..42fbeff738 100644
--- a/packages/frontend/.storybook/preload-theme.ts
+++ b/packages/frontend/.storybook/preload-theme.ts
@@ -1,6 +1,5 @@
 import { readFile, writeFile } from 'node:fs/promises';
-import { resolve } from 'node:path';
-import * as JSON5 from 'json5';
+import JSON5 from 'json5';
 
 const keys = [
 	'_dark',
@@ -26,9 +25,9 @@ const keys = [
 	'd-u0',
 ]
 
-Promise.all(keys.map((key) => readFile(resolve(__dirname, `../src/themes/${key}.json5`), 'utf8'))).then((sources) => {
+await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
 	writeFile(
-		resolve(__dirname, './themes.ts'),
+		new URL('./themes.ts', import.meta.url),
 		`export default ${JSON.stringify(
 			Object.fromEntries(sources.map((source, i) => [keys[i], JSON5.parse(source)])),
 			undefined,
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index e887acaa2e..67c81c666b 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -3,10 +3,10 @@ import { FORCE_REMOUNT } from '@storybook/core-events';
 import { type Preview, setup } from '@storybook/vue3';
 import isChromatic from 'chromatic/isChromatic';
 import { initialize, mswDecorator } from 'msw-storybook-addon';
-import { userDetailed } from './fakes';
-import locale from './locale';
-import { commonHandlers, onUnhandledRequest } from './mocks';
-import themes from './themes';
+import { userDetailed } from './fakes.js';
+import locale from './locale.js';
+import { commonHandlers, onUnhandledRequest } from './mocks.js';
+import themes from './themes.js';
 import '../src/style.scss';
 
 const appInitialized = Symbol();
diff --git a/packages/frontend/.storybook/tsconfig.json b/packages/frontend/.storybook/tsconfig.json
index 2db2f1eabe..02465f5afa 100644
--- a/packages/frontend/.storybook/tsconfig.json
+++ b/packages/frontend/.storybook/tsconfig.json
@@ -1,5 +1,7 @@
 {
 	"compilerOptions": {
+		"target": "es2022",
+		"module": "Node16",
 		"strict": true,
 		"allowUnusedLabels": false,
 		"allowUnreachableCode": false,
diff --git a/packages/sw/.eslintrc.js b/packages/sw/.eslintrc.cjs
similarity index 100%
rename from packages/sw/.eslintrc.js
rename to packages/sw/.eslintrc.cjs
diff --git a/packages/sw/build.js b/packages/sw/build.js
index ad16fb9497..e926197640 100644
--- a/packages/sw/build.js
+++ b/packages/sw/build.js
@@ -1,10 +1,13 @@
 // @ts-check
 
-const esbuild = require('esbuild');
-const locales = require('../../locales');
-const meta = require('../../package.json');
+import { fileURLToPath } from 'node:url';
+import * as esbuild from 'esbuild';
+import locales from '../../locales/index.js';
+import meta from '../../package.json' assert { type: "json" };
 const watch = process.argv[2]?.includes('watch');
 
+const __dirname = fileURLToPath(new URL('.', import.meta.url))
+
 console.log('Starting SW building...');
 
 /** @type {esbuild.BuildOptions} */
diff --git a/packages/sw/package.json b/packages/sw/package.json
index a2bead3dd4..a60f8230c2 100644
--- a/packages/sw/package.json
+++ b/packages/sw/package.json
@@ -19,5 +19,6 @@
 		"eslint": "8.44.0",
 		"eslint-plugin-import": "2.27.5",
 		"typescript": "5.1.6"
-	}
+	},
+	"type": "module"
 }
-- 
GitLab


From 9dd53527ca513f42c0ca9145e8122d898231f835 Mon Sep 17 00:00:00 2001
From: yupix <yupi0982@outlook.jp>
Date: Sun, 9 Jul 2023 17:20:50 +0900
Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=E3=83=97=E3=83=AD=E3=83=95?=
 =?UTF-8?q?=E3=82=A3=E3=83=BC=E3=83=ABURL=E3=82=92=E3=82=B3=E3=83=94?=
 =?UTF-8?q?=E3=83=BC=20=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92=E8=BF=BD?=
 =?UTF-8?q?=E5=8A=A0=20close=20#11190=20(#11205)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                   |  1 +
 locales/index.d.ts                             |  1 +
 locales/ja-JP.yml                              |  1 +
 packages/frontend/src/scripts/get-user-menu.ts | 10 +++++++++-
 4 files changed, 12 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c28d0b9bfb..e0d2fc7c3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
 - 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
 - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
 - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
+- プロフィールURLをコピーできるボタンを追加 #11190
 - Fix: サーバーメトリクスが90度傾いている
 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
diff --git a/locales/index.d.ts b/locales/index.d.ts
index dacab0cca5..42dddebbff 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -59,6 +59,7 @@ export interface Locale {
     "copyNoteId": string;
     "copyFileId": string;
     "copyFolderId": string;
+    "copyProfileUrl": string;
     "searchUser": string;
     "reply": string;
     "loadMore": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 29f157b849..16014da511 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -56,6 +56,7 @@ copyUserId: "ユーザーIDをコピー"
 copyNoteId: "ノートIDをコピー"
 copyFileId: "ファイルIDをコピー"
 copyFolderId: "フォルダーIDをコピー"
+copyProfileUrl: "プロフィールURLをコピー"
 searchUser: "ユーザーを検索"
 reply: "返信"
 loadMore: "もっと見る"
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index c884ed76cb..636a6543d0 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -2,13 +2,14 @@ import { defineAsyncComponent } from 'vue';
 import * as misskey from 'misskey-js';
 import { i18n } from '@/i18n';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { host } from '@/config';
+import { host, url } from '@/config';
 import * as os from '@/os';
 import { defaultStore, userActions } from '@/store';
 import { $i, iAmModerator } from '@/account';
 import { mainRouter } from '@/router';
 import { Router } from '@/nirax';
 import { rolesCache, userListsCache } from '@/cache';
+import { toUnicode } from 'punycode';
 
 export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -137,6 +138,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 		action: () => {
 			copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
 		},
+	}, {
+		icon: 'ti ti-share',
+		text: i18n.ts.copyProfileUrl,
+		action: () => {
+			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`
+			copyToClipboard(`${url}/${canonical}`);
+		},
 	}, {
 		icon: 'ti ti-mail',
 		text: i18n.ts.sendMessage,
-- 
GitLab


From 53b1684c39df28017e1f798ec5826569efcf143d Mon Sep 17 00:00:00 2001
From: eni <129673786+enitama@users.noreply.github.com>
Date: Sun, 9 Jul 2023 21:24:05 +0900
Subject: [PATCH 07/17] fix(frontend): use system-ui for system font (#11177)

* fix(frontend): correct system font stack

This was originally set to Hiragino Maru Gothic Pro, which is the same as the current default font.

* just use system-ui

per code review https://github.com/misskey-dev/misskey/pull/11177#discussion_r1257260039

---------

Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>
---
 CHANGELOG.md                     | 1 +
 packages/frontend/src/style.scss | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0d2fc7c3e..8e57baf2d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
 - Fix: ページ遷移でスクロール位置が保持されない問題を修正
 - Fix: フォルダーのページネーションが機能しない #11180
 - Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
+- Fix: システムフォント設定が正しく反映されない問題を修正
 
 ### Server
 - JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index b4b80a4bbe..bd74db7c85 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -72,7 +72,7 @@ html {
 	}
 
 	&.useSystemFont {
-		font-family: 'Hiragino Maru Gothic Pro', sans-serif;
+		font-family: system-ui;
 	}
 }
 
-- 
GitLab


From 1a096c557e6fd7bea2a3321fc99b42a8ad218270 Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Sun, 9 Jul 2023 22:46:17 +0900
Subject: [PATCH 08/17] refactor: fix lint failure (#11214)

---
 packages/frontend/src/scripts/get-user-menu.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 636a6543d0..bdcd1aabdc 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -142,7 +142,7 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 		icon: 'ti ti-share',
 		text: i18n.ts.copyProfileUrl,
 		action: () => {
-			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`
+			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
 			copyToClipboard(`${url}/${canonical}`);
 		},
 	}, {
-- 
GitLab


From 63e21a4ee3b4f97d17dedcec387ce5cdd2e5e518 Mon Sep 17 00:00:00 2001
From: akanevrc <93204493+akanevrc@users.noreply.github.com>
Date: Mon, 10 Jul 2023 13:26:05 +0900
Subject: [PATCH 09/17] =?UTF-8?q?fix(frontend):=20=E7=94=BB=E9=9D=A2?=
 =?UTF-8?q?=E3=83=93=E3=83=A5=E3=83=BC=E3=83=AF=E3=82=92=E3=82=BF=E3=83=83?=
 =?UTF-8?q?=E3=83=97=E3=81=97=E3=81=9F=E5=A0=B4=E5=90=88=E3=80=81=E3=83=9E?=
 =?UTF-8?q?=E3=82=A6=E3=82=B9=E3=82=AF=E3=83=AA=E3=83=83=E3=82=AF=E3=81=A8?=
 =?UTF-8?q?=E5=90=8C=E6=A7=98=E3=81=AB=E7=94=BB=E5=83=8F=E3=83=93=E3=83=A5?=
 =?UTF-8?q?=E3=83=BC=E3=83=AF=E3=82=92=E9=96=89=E3=81=98=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(#11211)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: change tapAction of photoswipe to 'close'

* doc: update CHANGELOG.md

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
---
 CHANGELOG.md                                     | 1 +
 packages/frontend/src/components/MkMediaList.vue | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e57baf2d7..95d512143b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
 - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
 - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
 - プロフィールURLをコピーできるボタンを追加 #11190
+- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
 - Fix: サーバーメトリクスが90度傾いている
 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 34f59c5573..be0aed6524 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -113,7 +113,7 @@ onMounted(() => {
 			right: 0,
 		},
 		imageClickAction: 'close',
-		tapAction: 'toggle-controls',
+		tapAction: 'close',
 		bgOpacity: 1,
 		showAnimationDuration: 100,
 		hideAnimationDuration: 100,
-- 
GitLab


From 239ea39d6f06c42d692b1ea8a874a8755a66dc15 Mon Sep 17 00:00:00 2001
From: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
Date: Mon, 10 Jul 2023 13:30:41 +0900
Subject: [PATCH 10/17] =?UTF-8?q?feat:=20=E3=83=95=E3=82=A9=E3=83=AD?=
 =?UTF-8?q?=E3=83=BC=E3=82=84=E3=81=8A=E6=B0=97=E3=81=AB=E5=85=A5=E3=82=8A?=
 =?UTF-8?q?=E7=99=BB=E9=8C=B2=E3=82=92=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D=E3=83=AB=E3=82=92?=
 =?UTF-8?q?=E9=96=8B=E3=81=8F=E6=99=82=E3=81=AF=E6=A6=82=E8=A6=81=E3=83=9A?=
 =?UTF-8?q?=E3=83=BC=E3=82=B8=E3=82=92=E9=96=8B=E3=81=8F=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=20(#11218)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように

* Update CHANGELOG.md

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
---
 CHANGELOG.md                            | 1 +
 packages/frontend/src/pages/channel.vue | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 95d512143b..959954b661 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
 - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
 - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
 - プロフィールURLをコピーできるボタンを追加 #11190
+- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
 - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
 - Fix: サーバーメトリクスが90度傾いている
 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index bcc0fc6860..2a056f21d4 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -87,7 +87,7 @@ const props = defineProps<{
 	channelId: string;
 }>();
 
-let tab = $ref('timeline');
+let tab = $ref('overview');
 let channel = $ref(null);
 let favorited = $ref(false);
 let searchQuery = $ref('');
@@ -107,6 +107,9 @@ watch(() => props.channelId, async () => {
 		channelId: props.channelId,
 	});
 	favorited = channel.isFavorited;
+	if (favorited || channel.isFollowing) {
+		tab = 'timeline';
+	}
 }, { immediate: true });
 
 function edit() {
-- 
GitLab


From f4d1fcaf67716aa710d271e7f56c4f78cd202f29 Mon Sep 17 00:00:00 2001
From: yupix <yupi0982@outlook.jp>
Date: Mon, 10 Jul 2023 15:55:10 +0900
Subject: [PATCH 11/17] =?UTF-8?q?feat:=20=E3=83=A6=E3=83=BC=E3=82=B6?=
 =?UTF-8?q?=E3=83=BC=E3=82=92contextmenu=E3=81=8B=E3=82=89=E3=82=A2?=
 =?UTF-8?q?=E3=83=B3=E3=83=86=E3=83=8A=E3=81=AB=E8=BF=BD=E5=8A=A0=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=AA=E3=81=A9?=
 =?UTF-8?q?=20(#11206)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: ユーザーをcontextmenuからアンテナに追加できるように close #11115

* MkAvatars.vue変更

* nanka iroiro

* fix MkAvatars

* ix

* fix

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
---
 CHANGELOG.md                                  |  1 +
 locales/index.d.ts                            |  1 +
 locales/ja-JP.yml                             |  1 +
 packages/frontend/src/cache.ts                |  1 +
 .../frontend/src/components/MkAvatars.vue     | 15 ++--
 .../frontend/src/pages/my-antennas/create.vue |  6 +-
 .../frontend/src/pages/my-antennas/edit.vue   |  6 +-
 .../frontend/src/pages/my-antennas/index.vue  | 89 +++++++++++--------
 .../frontend/src/pages/my-lists/index.vue     | 53 +++++++----
 packages/frontend/src/scripts/cache.ts        | 12 +--
 .../frontend/src/scripts/get-user-menu.ts     | 36 +++++++-
 11 files changed, 144 insertions(+), 77 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 959954b661..a03ca4ab86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@
 - フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
 - 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
 - プロフィールURLをコピーできるボタンを追加 #11190
+- ユーザーのContextMenuに「アンテナに追加」ボタンを追加
 - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
 - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
 - Fix: サーバーメトリクスが90度傾いている
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 42dddebbff..8697b22e21 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -52,6 +52,7 @@ export interface Locale {
     "deleteAndEdit": string;
     "deleteAndEditConfirm": string;
     "addToList": string;
+    "addToAntenna": string;
     "sendMessage": string;
     "copyRSS": string;
     "copyUsername": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 16014da511..82efc8a469 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -49,6 +49,7 @@ delete: "削除"
 deleteAndEdit: "削除して編集"
 deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。"
 addToList: "リストに追加"
+addToAntenna: "アンテナに追加"
 sendMessage: "メッセージを送信"
 copyRSS: "RSSをコピー"
 copyUsername: "ユーザー名をコピー"
diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts
index c95da64bba..cb315c8ff7 100644
--- a/packages/frontend/src/cache.ts
+++ b/packages/frontend/src/cache.ts
@@ -4,3 +4,4 @@ import { Cache } from '@/scripts/cache';
 export const clipsCache = new Cache<misskey.entities.Clip[]>(Infinity);
 export const rolesCache = new Cache(Infinity);
 export const userListsCache = new Cache<misskey.entities.UserList[]>(Infinity);
+export const antennasCache = new Cache<misskey.entities.Antenna[]>(Infinity);
diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue
index 630620fc08..437dce0a14 100644
--- a/packages/frontend/src/components/MkAvatars.vue
+++ b/packages/frontend/src/components/MkAvatars.vue
@@ -1,24 +1,29 @@
 <template>
 <div>
-	<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
+	<div v-for="user in users.slice(0, limit)" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
 		<MkAvatar :user="user" style="width:32px; height:32px;" indicator link preview/>
 	</div>
+	<div v-if="users.length > limit" style="display: inline-block;">...</div>
 </div>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as os from '@/os';
+import { UserLite } from 'misskey-js/built/entities';
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	userIds: string[];
-}>();
+	limit?: number;
+}>(), {
+	limit: Infinity,
+});
 
-const users = ref([]);
+const users = ref<UserLite[]>([]);
 
 onMounted(async () => {
 	users.value = await os.api('users/show', {
 		userIds: props.userIds,
-	});
+	}) as unknown as UserLite[];
 });
 </script>
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index 355d18fdb5..632c36bbf8 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -9,6 +9,7 @@ import XAntenna from './editor.vue';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { useRouter } from '@/router';
+import { antennasCache } from '@/cache';
 
 const router = useRouter();
 
@@ -26,13 +27,10 @@ let draft = $ref({
 });
 
 function onAntennaCreated() {
+	antennasCache.delete();
 	router.push('/my/antennas');
 }
 
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
 definePageMetadata({
 	title: i18n.ts.manageAntennas,
 	icon: 'ti ti-antenna',
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index da9b2de48f..3fb9690ac1 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -10,6 +10,7 @@ import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { useRouter } from '@/router';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { antennasCache } from '@/cache';
 
 const router = useRouter();
 
@@ -20,6 +21,7 @@ const props = defineProps<{
 }>();
 
 function onAntennaUpdated() {
+	antennasCache.delete();
 	router.push('/my/antennas');
 }
 
@@ -27,10 +29,6 @@ os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) =
 	antenna = antennaResponse;
 });
 
-const headerActions = $computed(() => []);
-
-const headerTabs = $computed(() => []);
-
 definePageMetadata({
 	title: i18n.ts.manageAntennas,
 	icon: 'ti ti-antenna',
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 2ca026b9a1..1e9136f1fa 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -2,15 +2,20 @@
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700">
-		<div class="ieepwinx">
-			<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+		<div>
+			<div v-if="antennas.length === 0" class="empty">
+				<div class="_fullinfo">
+					<img :src="infoImageUrl" class="_ghost"/>
+					<div>{{ i18n.ts.nothing }}</div>
+				</div>
+			</div>
+
+			<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
 
-			<div class="">
-				<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
-					<MkA v-for="antenna in items" :key="antenna.id" class="ljoevbzj" :to="`/my/antennas/${antenna.id}`">
-						<div class="name">{{ antenna.name }}</div>
-					</MkA>
-				</MkPagination>
+			<div v-if="antennas.length > 0" class="_gaps">
+				<MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`">
+					<div class="name">{{ antenna.name }}</div>
+				</MkA>
 			</div>
 		</div>
 	</MkSpacer>
@@ -18,19 +23,31 @@
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
-import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { antennasCache } from '@/cache';
+import { api } from '@/os';
+import { onActivated } from 'vue';
+import { infoImageUrl } from '@/instance';
 
-const pagination = {
-	endpoint: 'antennas/list' as const,
-	noPaging: true,
-	limit: 10,
-};
+const antennas = $computed(() => antennasCache.value.value ?? []);
+
+function fetch() {
+	antennasCache.fetch(() => api('antennas/list'));
+}
 
-const headerActions = $computed(() => []);
+fetch();
+
+const headerActions = $computed(() => [{
+	asFullButton: true,
+	icon: 'ti ti-refresh',
+	text: i18n.ts.reload,
+	handler: () => {
+		antennasCache.delete();
+		fetch();
+	},
+}]);
 
 const headerTabs = $computed(() => []);
 
@@ -38,30 +55,30 @@ definePageMetadata({
 	title: i18n.ts.manageAntennas,
 	icon: 'ti ti-antenna',
 });
-</script>
-
-<style lang="scss" scoped>
-.ieepwinx {
 
-	> .add {
-		margin: 0 auto 16px auto;
-	}
+onActivated(() => {
+	antennasCache.fetch(() => api('antennas/list'));
+});
+</script>
 
-	.ljoevbzj {
-		display: block;
-		padding: 16px;
-		margin-bottom: 8px;
-		border: solid 1px var(--divider);
-		border-radius: 6px;
+<style lang="scss" module>
+.add {
+	margin: 0 auto 16px auto;
+}
 
-		&:hover {
-			border: solid 1px var(--accent);
-			text-decoration: none;
-		}
+.antenna {
+	display: block;
+	padding: 16px;
+	border: solid 1px var(--divider);
+	border-radius: 6px;
 
-		> .name {
-			font-weight: bold;
-		}
+	&:hover {
+		border: solid 1px var(--accent);
+		text-decoration: none;
 	}
 }
+
+.name {
+	font-weight: bold;
+}
 </style>
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index cee241c489..0f59ca0b36 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -3,38 +3,43 @@
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700">
 		<div class="_gaps">
+			<div v-if="items.length === 0" class="empty">
+				<div class="_fullinfo">
+					<img :src="infoImageUrl" class="_ghost"/>
+					<div>{{ i18n.ts.nothing }}</div>
+				</div>
+			</div>
+
 			<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
 
-			<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination">
-				<div class="_gaps">
-					<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
-						<div style="margin-bottom: 4px;">{{ list.name }}</div>
-						<MkAvatars :userIds="list.userIds"/>
-					</MkA>
-				</div>
-			</MkPagination>
+			<div v-if="items.length > 0" class="_gaps">
+				<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
+					<div style="margin-bottom: 4px;">{{ list.name }}</div>
+					<MkAvatars :userIds="list.userIds" :limit="10"/>
+				</MkA>
+			</div>
 		</div>
 	</MkSpacer>
 </MkStickyContainer>
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
-import MkPagination from '@/components/MkPagination.vue';
+import { onActivated } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkAvatars from '@/components/MkAvatars.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { userListsCache } from '@/cache';
+import { infoImageUrl } from '@/instance';
+
+const items = $computed(() => userListsCache.value.value ?? []);
 
-const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
+function fetch() {
+	userListsCache.fetch(() => os.api('users/lists/list'));
+}
 
-const pagination = {
-	endpoint: 'users/lists/list' as const,
-	noPaging: true,
-	limit: 10,
-};
+fetch();
 
 async function create() {
 	const { canceled, result: name } = await os.inputText({
@@ -43,10 +48,18 @@ async function create() {
 	if (canceled) return;
 	await os.apiWithDialog('users/lists/create', { name: name });
 	userListsCache.delete();
-	pagingComponent.reload();
+	fetch();
 }
 
-const headerActions = $computed(() => []);
+const headerActions = $computed(() => [{
+	asFullButton: true,
+	icon: 'ti ti-refresh',
+	text: i18n.ts.reload,
+	handler: () => {
+		userListsCache.delete();
+		fetch();
+	},
+}]);
 
 const headerTabs = $computed(() => []);
 
@@ -58,6 +71,10 @@ definePageMetadata({
 		handler: create,
 	},
 });
+
+onActivated(() => {
+	fetch();
+});
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts
index 858e5f03bf..a61d858353 100644
--- a/packages/frontend/src/scripts/cache.ts
+++ b/packages/frontend/src/scripts/cache.ts
@@ -1,7 +1,8 @@
+import { ref } from "vue";
 
 export class Cache<T> {
 	private cachedAt: number | null = null;
-	private value: T | undefined;
+	public value = ref<T | undefined>();
 	private lifetime: number;
 
 	constructor(lifetime: Cache<never>['lifetime']) {
@@ -10,21 +11,20 @@ export class Cache<T> {
 
 	public set(value: T): void {
 		this.cachedAt = Date.now();
-		this.value = value;
+		this.value.value = value;
 	}
 
-	public get(): T | undefined {
+	private get(): T | undefined {
 		if (this.cachedAt == null) return undefined;
 		if ((Date.now() - this.cachedAt) > this.lifetime) {
-			this.value = undefined;
+			this.value.value = undefined;
 			this.cachedAt = null;
 			return undefined;
 		}
-		return this.value;
+		return this.value.value;
 	}
 
 	public delete() {
-		this.value = undefined;
 		this.cachedAt = null;
 	}
 
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index bdcd1aabdc..1c93d58b44 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -1,3 +1,4 @@
+import { toUnicode } from 'punycode';
 import { defineAsyncComponent } from 'vue';
 import * as misskey from 'misskey-js';
 import { i18n } from '@/i18n';
@@ -8,8 +9,7 @@ import { defaultStore, userActions } from '@/store';
 import { $i, iAmModerator } from '@/account';
 import { mainRouter } from '@/router';
 import { Router } from '@/nirax';
-import { rolesCache, userListsCache } from '@/cache';
-import { toUnicode } from 'punycode';
+import { antennasCache, rolesCache, userListsCache } from '@/cache';
 
 export function getUserMenu(user: misskey.entities.UserDetailed, router: Router = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -166,11 +166,39 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 
 			return lists.map(list => ({
 				text: list.name,
-				action: () => {
-					os.apiWithDialog('users/lists/push', {
+				action: async () => {
+					await os.apiWithDialog('users/lists/push', {
 						listId: list.id,
 						userId: user.id,
 					});
+					userListsCache.delete();
+				},
+			}));
+		},
+	}, {
+		type: 'parent',
+		icon: 'ti ti-antenna',
+		text: i18n.ts.addToAntenna,
+		children: async () => {
+			const antennas = await antennasCache.fetch(() => os.api('antennas/list'));
+			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
+			return antennas.filter((a) => a.src === 'users').map(antenna => ({
+				text: antenna.name,
+				action: async () => {
+					await os.apiWithDialog('antennas/update', {
+						antennaId: antenna.id,
+						name: antenna.name,
+						keywords: antenna.keywords,
+						excludeKeywords: antenna.excludeKeywords,
+						src: antenna.src,
+						userListId: antenna.userListId,
+						users: [...antenna.users, canonical],
+						caseSensitive: antenna.caseSensitive,
+						withReplies: antenna.withReplies,
+						withFile: antenna.withFile,
+						notify: antenna.notify,
+					});
+					antennasCache.delete();
 				},
 			}));
 		},
-- 
GitLab


From 791ae608a50f9f7abab048185ea146823d285582 Mon Sep 17 00:00:00 2001
From: nomad <benkang666@gmail.com>
Date: Tue, 11 Jul 2023 13:40:56 +0800
Subject: [PATCH 12/17] fix(backend): fix fetchInstanceMetadata error (#11236)

---
 packages/backend/src/core/FetchInstanceMetadataService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index a0afaaf888..9e8d17442f 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -103,7 +103,7 @@ export class FetchInstanceMetadataService {
 
 			if (name) updates.name = name;
 			if (description) updates.description = description;
-			if (icon || favicon) updates.iconUrl = icon ?? favicon;
+			if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
 			if (favicon) updates.faviconUrl = favicon;
 			if (themeColor) updates.themeColor = themeColor;
 
-- 
GitLab


From 48d33414627bd7d8208939a0119abf0e6f336800 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Tue, 11 Jul 2023 05:56:56 +0000
Subject: [PATCH 13/17] chore(frontend): Remove experimental flag from
 migration feature

---
 packages/frontend/src/pages/settings/index.vue     | 2 +-
 packages/frontend/src/pages/settings/migration.vue | 3 ---
 2 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index b4f056d8a6..d53519e0d5 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -166,7 +166,7 @@ const menuDef = computed(() => [{
 		active: currentPage?.route.name === 'import-export',
 	}, {
 		icon: 'ti ti-plane',
-		text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`,
+		text: `${i18n.ts.accountMigration}`,
 		to: '/settings/migration',
 		active: currentPage?.route.name === 'migration',
 	}, {
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 102bc68523..38e0d0abb2 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -1,8 +1,5 @@
 <template>
 <div class="_gaps_m">
-	<FormInfo warn>
-		{{ i18n.ts.thisIsExperimentalFeature }}
-	</FormInfo>
 	<MkFolder :defaultOpen="true">
 		<template #icon><i class="ti ti-plane-arrival"></i></template>
 		<template #label>{{ i18n.ts._accountMigration.moveFrom }}</template>
-- 
GitLab


From cf3e39178b4180bfc5f57b8bb766faf5b5fc99b9 Mon Sep 17 00:00:00 2001
From: okayurisotto <okayurisotto@proton.me>
Date: Tue, 11 Jul 2023 14:58:58 +0900
Subject: [PATCH 14/17] =?UTF-8?q?refactor(backend):=20=E5=AD=98=E5=9C=A8?=
 =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E3=81=AE`findOneBy`=E3=82=92`exist`=E3=81=AB?=
 =?UTF-8?q?=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88=20(#11224)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor(backend): 存在確認の`findOneBy`を`exist`に置き換え

* cleanup
---
 .../backend/src/core/NoteCreateService.ts     | 20 ++++++----
 packages/backend/src/core/NoteReadService.ts  | 14 ++++---
 packages/backend/src/core/SignupService.ts    |  4 +-
 .../backend/src/core/UserFollowingService.ts  | 40 +++++++++++--------
 .../src/core/activitypub/ApInboxService.ts    | 30 ++++++++------
 .../src/core/activitypub/ApRendererService.ts |  4 +-
 .../src/core/entities/ChannelEntityService.ts | 31 +++++++++-----
 .../src/core/entities/ClipEntityService.ts    |  2 +-
 .../src/core/entities/FlashEntityService.ts   |  2 +-
 .../core/entities/GalleryPostEntityService.ts |  2 +-
 .../src/core/entities/NoteEntityService.ts    | 14 +++----
 .../src/core/entities/PageEntityService.ts    |  2 +-
 .../src/core/entities/UserEntityService.ts    | 14 ++++---
 .../src/server/api/SignupApiService.ts        |  4 +-
 .../api/endpoints/admin/promo/create.ts       |  4 +-
 .../api/endpoints/admin/roles/update.ts       |  4 +-
 .../src/server/api/endpoints/auth/accept.ts   | 10 +++--
 .../server/api/endpoints/blocking/create.ts   | 10 +++--
 .../server/api/endpoints/blocking/delete.ts   | 10 +++--
 .../server/api/endpoints/clips/add-note.ts    | 10 +++--
 .../server/api/endpoints/clips/favorite.ts    | 10 +++--
 .../endpoints/drive/files/check-existence.ts  | 10 +++--
 .../src/server/api/endpoints/flash/like.ts    | 10 +++--
 .../server/api/endpoints/following/create.ts  | 10 +++--
 .../server/api/endpoints/following/delete.ts  | 10 +++--
 .../api/endpoints/gallery/posts/like.ts       | 10 +++--
 .../server/api/endpoints/i/import-antennas.ts |  4 +-
 .../api/endpoints/i/read-announcement.ts      | 14 ++++---
 .../server/api/endpoints/i/revoke-token.ts    |  4 +-
 .../src/server/api/endpoints/mute/create.ts   | 10 +++--
 .../src/server/api/endpoints/notes/create.ts  | 20 ++++++----
 .../api/endpoints/notes/favorites/create.ts   | 10 +++--
 .../src/server/api/endpoints/pages/like.ts    | 10 +++--
 .../src/server/api/endpoints/promo/read.ts    | 10 +++--
 .../server/api/endpoints/users/followers.ts   | 10 +++--
 .../server/api/endpoints/users/following.ts   | 10 +++--
 .../users/lists/create-from-public.ts         | 28 ++++++++-----
 .../api/endpoints/users/lists/favorite.ts     | 20 ++++++----
 .../server/api/endpoints/users/lists/push.ts  | 18 +++++----
 .../server/api/endpoints/users/lists/show.ts  | 10 +++--
 .../api/endpoints/users/lists/unfavorite.ts   | 10 +++--
 .../server/api/stream/channels/user-list.ts   | 10 +++--
 42 files changed, 288 insertions(+), 201 deletions(-)

diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index fb36ca3b6a..4f3d66dd58 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -570,12 +570,14 @@ export class NoteCreateService implements OnApplicationShutdown {
 			if (data.reply) {
 				// 通知
 				if (data.reply.userHost === null) {
-					const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
-						userId: data.reply.userId,
-						threadId: data.reply.threadId ?? data.reply.id,
+					const isThreadMuted = await this.noteThreadMutingsRepository.exist({
+						where: {
+							userId: data.reply.userId,
+							threadId: data.reply.threadId ?? data.reply.id,
+						}
 					});
 
-					if (!threadMuted) {
+					if (!isThreadMuted) {
 						nm.push(data.reply.userId, 'reply');
 						this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
 
@@ -712,12 +714,14 @@ export class NoteCreateService implements OnApplicationShutdown {
 	@bindThis
 	private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
 		for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
-			const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
-				userId: u.id,
-				threadId: note.threadId ?? note.id,
+			const isThreadMuted = await this.noteThreadMutingsRepository.exist({
+				where: {
+					userId: u.id,
+					threadId: note.threadId ?? note.id,
+				},
 			});
 
-			if (threadMuted) {
+			if (isThreadMuted) {
 				continue;
 			}
 
diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts
index b84591e26d..52e9bd369a 100644
--- a/packages/backend/src/core/NoteReadService.ts
+++ b/packages/backend/src/core/NoteReadService.ts
@@ -43,11 +43,13 @@ export class NoteReadService implements OnApplicationShutdown {
 		//#endregion
 
 		// スレッドミュート
-		const threadMute = await this.noteThreadMutingsRepository.findOneBy({
-			userId: userId,
-			threadId: note.threadId ?? note.id,
+		const isThreadMuted = await this.noteThreadMutingsRepository.exist({
+			where: {
+				userId: userId,
+				threadId: note.threadId ?? note.id,
+			},
 		});
-		if (threadMute) return;
+		if (isThreadMuted) return;
 
 		const unread = {
 			id: this.idService.genId(),
@@ -62,9 +64,9 @@ export class NoteReadService implements OnApplicationShutdown {
 
 		// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
 		setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
-			const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id });
+			const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
 
-			if (exist == null) return;
+			if (!exist) return;
 
 			if (params.isMentioned) {
 				this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index e6b17a8741..1e44406c16 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -71,12 +71,12 @@ export class SignupService {
 		const secret = generateUserToken();
 
 		// Check username duplication
-		if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
+		if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
 			throw new Error('DUPLICATED_USERNAME');
 		}
 
 		// Check deleted username duplication
-		if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
+		if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
 			throw new Error('USED_USERNAME');
 		}
 
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 7d90bc2c08..4d7bfeaf23 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -122,22 +122,26 @@ export class UserFollowingService implements OnModuleInit {
 			let autoAccept = false;
 
 			// 鍵アカウントであっても、既にフォローされていた場合はスルー
-			const following = await this.followingsRepository.findOneBy({
-				followerId: follower.id,
-				followeeId: followee.id,
+			const isFollowing = await this.followingsRepository.exist({
+				where: {
+					followerId: follower.id,
+					followeeId: followee.id,
+				},
 			});
-			if (following) {
+			if (isFollowing) {
 				autoAccept = true;
 			}
 
 			// フォローしているユーザーは自動承認オプション
 			if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
-				const followed = await this.followingsRepository.findOneBy({
-					followerId: followee.id,
-					followeeId: follower.id,
+				const isFollowed = await this.followingsRepository.exist({
+					where: {
+						followerId: followee.id,
+						followeeId: follower.id,
+					},
 				});
 
-				if (followed) autoAccept = true;
+				if (isFollowed) autoAccept = true;
 			}
 
 			// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
@@ -206,12 +210,14 @@ export class UserFollowingService implements OnModuleInit {
 
 		this.cacheService.userFollowingsCache.refresh(follower.id);
 
-		const req = await this.followRequestsRepository.findOneBy({
-			followeeId: followee.id,
-			followerId: follower.id,
+		const requestExist = await this.followRequestsRepository.exist({
+			where: {
+				followeeId: followee.id,
+				followerId: follower.id,
+			},
 		});
 
-		if (req) {
+		if (requestExist) {
 			await this.followRequestsRepository.delete({
 				followeeId: followee.id,
 				followerId: follower.id,
@@ -505,12 +511,14 @@ export class UserFollowingService implements OnModuleInit {
 			}
 		}
 
-		const request = await this.followRequestsRepository.findOneBy({
-			followeeId: followee.id,
-			followerId: follower.id,
+		const requestExist = await this.followRequestsRepository.exist({
+			where: {
+				followeeId: followee.id,
+				followerId: follower.id,
+			},
 		});
 
-		if (request == null) {
+		if (!requestExist) {
 			throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
 		}
 
diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts
index efef777fb0..b0428763c9 100644
--- a/packages/backend/src/core/activitypub/ApInboxService.ts
+++ b/packages/backend/src/core/activitypub/ApInboxService.ts
@@ -618,12 +618,14 @@ export class ApInboxService {
 			return 'skip: follower not found';
 		}
 
-		const following = await this.followingsRepository.findOneBy({
-			followerId: follower.id,
-			followeeId: actor.id,
+		const isFollowing = await this.followingsRepository.exist({
+			where: {
+				followerId: follower.id,
+				followeeId: actor.id,
+			},
 		});
 
-		if (following) {
+		if (isFollowing) {
 			await this.userFollowingService.unfollow(follower, actor);
 			return 'ok: unfollowed';
 		}
@@ -673,22 +675,26 @@ export class ApInboxService {
 			return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
 		}
 
-		const req = await this.followRequestsRepository.findOneBy({
-			followerId: actor.id,
-			followeeId: followee.id,
+		const requestExist = await this.followRequestsRepository.exist({
+			where: {
+				followerId: actor.id,
+				followeeId: followee.id,
+			},
 		});
 
-		const following = await this.followingsRepository.findOneBy({
-			followerId: actor.id,
-			followeeId: followee.id,
+		const isFollowing = await this.followingsRepository.exist({
+			where: {
+				followerId: actor.id,
+				followeeId: followee.id,
+			},
 		});
 
-		if (req) {
+		if (requestExist) {
 			await this.userFollowingService.cancelFollowRequest(followee, actor);
 			return 'ok: follow request canceled';
 		}
 
-		if (following) {
+		if (isFollowing) {
 			await this.userFollowingService.unfollow(actor, followee);
 			return 'ok: unfollowed';
 		}
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index d8b95ca4d1..96ac5c61f1 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -323,9 +323,9 @@ export class ApRendererService {
 			inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
 
 			if (inReplyToNote != null) {
-				const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
+				const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
 
-				if (inReplyToUser != null) {
+				if (inReplyToUserExist) {
 					if (inReplyToNote.uri) {
 						inReplyTo = inReplyToNote.uri;
 					} else {
diff --git a/packages/backend/src/core/entities/ChannelEntityService.ts b/packages/backend/src/core/entities/ChannelEntityService.ts
index 15ffd44861..d24657260f 100644
--- a/packages/backend/src/core/entities/ChannelEntityService.ts
+++ b/packages/backend/src/core/entities/ChannelEntityService.ts
@@ -47,17 +47,26 @@ export class ChannelEntityService {
 
 		const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
 
-		const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
+		const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
+			where: {
+				noteChannelId: channel.id,
+				userId: meId
+			},
+		}) : undefined;
 
-		const following = meId ? await this.channelFollowingsRepository.findOneBy({
-			followerId: meId,
-			followeeId: channel.id,
-		}) : null;
+		const isFollowing = meId ? await this.channelFollowingsRepository.exist({
+			where: {
+				followerId: meId,
+				followeeId: channel.id,
+			},
+		}) : false;
 
-		const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
-			userId: meId,
-			channelId: channel.id,
-		}) : null;
+		const isFavorited = meId ? await this.channelFavoritesRepository.exist({
+			where: {
+				userId: meId,
+				channelId: channel.id,
+			},
+		}) : false;
 
 		const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
 			where: {
@@ -80,8 +89,8 @@ export class ChannelEntityService {
 			notesCount: channel.notesCount,
 
 			...(me ? {
-				isFollowing: following != null,
-				isFavorited: favorite != null,
+				isFollowing,
+				isFavorited,
 				hasUnreadNote,
 			} : {}),
 
diff --git a/packages/backend/src/core/entities/ClipEntityService.ts b/packages/backend/src/core/entities/ClipEntityService.ts
index 33d3c53806..f558cbc33d 100644
--- a/packages/backend/src/core/entities/ClipEntityService.ts
+++ b/packages/backend/src/core/entities/ClipEntityService.ts
@@ -39,7 +39,7 @@ export class ClipEntityService {
 			description: clip.description,
 			isPublic: clip.isPublic,
 			favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
-			isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
+			isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
 		});
 	}
 
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index e52a591884..92345457c9 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -40,7 +40,7 @@ export class FlashEntityService {
 			summary: flash.summary,
 			script: flash.script,
 			likedCount: flash.likedCount,
-			isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
+			isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
 		});
 	}
 
diff --git a/packages/backend/src/core/entities/GalleryPostEntityService.ts b/packages/backend/src/core/entities/GalleryPostEntityService.ts
index 632c75304f..c44a5df118 100644
--- a/packages/backend/src/core/entities/GalleryPostEntityService.ts
+++ b/packages/backend/src/core/entities/GalleryPostEntityService.ts
@@ -46,7 +46,7 @@ export class GalleryPostEntityService {
 			tags: post.tags.length > 0 ? post.tags : undefined,
 			isSensitive: post.isSensitive,
 			likedCount: post.likedCount,
-			isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
+			isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
 		});
 	}
 
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index e730c0eb8f..546e5f56d2 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -106,16 +106,14 @@ export class NoteEntityService implements OnModuleInit {
 				hide = false;
 			} else {
 			// フォロワーかどうか
-				const following = await this.followingsRepository.findOneBy({
-					followeeId: packedNote.userId,
-					followerId: meId,
+				const isFollowing = await this.followingsRepository.exist({
+					where: {
+						followeeId: packedNote.userId,
+						followerId: meId,
+					},
 				});
 
-				if (following == null) {
-					hide = true;
-				} else {
-					hide = false;
-				}
+				hide = !isFollowing;
 			}
 		}
 
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index d6da856637..94b26a5017 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -97,7 +97,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 DriveFile => x != null)),
 			likedCount: page.likedCount,
-			isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
+			isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
 		});
 	}
 
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 6dacaa1032..7d248f8524 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -230,12 +230,14 @@ export class UserEntityService implements OnModuleInit {
 		/*
 		const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
 
-		const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
-			antennaId: In(myAntennas.map(x => x.id)),
-			read: false,
-		}) : null;
-
-		return unread != null;
+		const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
+			where: {
+				antennaId: In(myAntennas.map(x => x.id)),
+				read: false,
+			},
+		}) : false);
+
+		return isUnread;
 		*/
 		return false; // TODO
 	}
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index fc5f3811eb..5e18dcbe08 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -128,12 +128,12 @@ export class SignupApiService {
 		}
 
 		if (instance.emailRequiredForSignup) {
-			if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
+			if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
 				throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
 			}
 
 			// Check deleted username duplication
-			if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
+			if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
 				throw new FastifyReplyError(400, 'USED_USERNAME');
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/admin/promo/create.ts b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
index bee1ffbaee..8401cf51d9 100644
--- a/packages/backend/src/server/api/endpoints/admin/promo/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/promo/create.ts
@@ -50,9 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw e;
 			});
 
-			const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id });
+			const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyPromoted);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
index 467f157a61..1fedab4540 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -69,8 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps) => {
-			const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
-			if (role == null) {
+			const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
+			if (!roleExist) {
 				throw new ApiError(meta.errors.noSuchRole);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/auth/accept.ts b/packages/backend/src/server/api/endpoints/auth/accept.ts
index e69f9c12e2..aa199ab730 100644
--- a/packages/backend/src/server/api/endpoints/auth/accept.ts
+++ b/packages/backend/src/server/api/endpoints/auth/accept.ts
@@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			const accessToken = secureRndstr(32);
 
 			// Fetch exist access token
-			const exist = await this.accessTokensRepository.findOneBy({
-				appId: session.appId,
-				userId: me.id,
+			const exist = await this.accessTokensRepository.exist({
+				where: {
+					appId: session.appId,
+					userId: me.id,
+				},
 			});
 
-			if (exist == null) {
+			if (!exist) {
 				const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
 
 				// Generate Hash
diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts
index d9ba99f209..4ad40c8f1c 100644
--- a/packages/backend/src/server/api/endpoints/blocking/create.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/create.ts
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			// Check if already blocking
-			const exist = await this.blockingsRepository.findOneBy({
-				blockerId: blocker.id,
-				blockeeId: blockee.id,
+			const exist = await this.blockingsRepository.exist({
+				where: {
+					blockerId: blocker.id,
+					blockeeId: blockee.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyBlocking);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts
index 46dd26a45a..38913ae932 100644
--- a/packages/backend/src/server/api/endpoints/blocking/delete.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			// Check not blocking
-			const exist = await this.blockingsRepository.findOneBy({
-				blockerId: blocker.id,
-				blockeeId: blockee.id,
+			const exist = await this.blockingsRepository.exist({
+				where: {
+					blockerId: blocker.id,
+					blockeeId: blockee.id,
+				}
 			});
 
-			if (exist == null) {
+			if (!exist) {
 				throw new ApiError(meta.errors.notBlocking);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/clips/add-note.ts b/packages/backend/src/server/api/endpoints/clips/add-note.ts
index c3561e2a71..2837f2cf81 100644
--- a/packages/backend/src/server/api/endpoints/clips/add-note.ts
+++ b/packages/backend/src/server/api/endpoints/clips/add-note.ts
@@ -87,12 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw e;
 			});
 
-			const exist = await this.clipNotesRepository.findOneBy({
-				noteId: note.id,
-				clipId: clip.id,
+			const exist = await this.clipNotesRepository.exist({
+				where: {
+					noteId: note.id,
+					clipId: clip.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyClipped);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts
index f08caaf8d7..ce09855531 100644
--- a/packages/backend/src/server/api/endpoints/clips/favorite.ts
+++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts
@@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw new ApiError(meta.errors.noSuchClip);
 			}
 
-			const exist = await this.clipFavoritesRepository.findOneBy({
-				clipId: clip.id,
-				userId: me.id,
+			const exist = await this.clipFavoritesRepository.exist({
+				where: {
+					clipId: clip.id,
+					userId: me.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyFavorited);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
index 290cd4d2ce..cdcdde7e8a 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/check-existence.ts
@@ -34,12 +34,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private driveFilesRepository: DriveFilesRepository,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const file = await this.driveFilesRepository.findOneBy({
-				md5: ps.md5,
-				userId: me.id,
+			const exist = await this.driveFilesRepository.exist({
+				where: {
+					md5: ps.md5,
+					userId: me.id,
+				},
 			});
 
-			return file != null;
+			return exist;
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/flash/like.ts b/packages/backend/src/server/api/endpoints/flash/like.ts
index 23de2f3970..57245f9f41 100644
--- a/packages/backend/src/server/api/endpoints/flash/like.ts
+++ b/packages/backend/src/server/api/endpoints/flash/like.ts
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 
 			// if already liked
-			const exist = await this.flashLikesRepository.findOneBy({
-				flashId: flash.id,
-				userId: me.id,
+			const exist = await this.flashLikesRepository.exist({
+				where: {
+					flashId: flash.id,
+					userId: me.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyLiked);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts
index 4ad16de911..009fc96f64 100644
--- a/packages/backend/src/server/api/endpoints/following/create.ts
+++ b/packages/backend/src/server/api/endpoints/following/create.ts
@@ -99,12 +99,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			// Check if already following
-			const exist = await this.followingsRepository.findOneBy({
-				followerId: follower.id,
-				followeeId: followee.id,
+			const exist = await this.followingsRepository.exist({
+				where: {
+					followerId: follower.id,
+					followeeId: followee.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyFollowing);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/following/delete.ts b/packages/backend/src/server/api/endpoints/following/delete.ts
index 4f12db1273..570c4eb81e 100644
--- a/packages/backend/src/server/api/endpoints/following/delete.ts
+++ b/packages/backend/src/server/api/endpoints/following/delete.ts
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			// Check not following
-			const exist = await this.followingsRepository.findOneBy({
-				followerId: follower.id,
-				followeeId: followee.id,
+			const exist = await this.followingsRepository.exist({
+				where: {
+					followerId: follower.id,
+					followeeId: followee.id,
+				},
 			});
 
-			if (exist == null) {
+			if (!exist) {
 				throw new ApiError(meta.errors.notFollowing);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
index 6ac5fa8606..c0bb55f640 100644
--- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
+++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 
 			// if already liked
-			const exist = await this.galleryLikesRepository.findOneBy({
-				postId: post.id,
-				userId: me.id,
+			const exist = await this.galleryLikesRepository.exist({
+				where: {
+					postId: post.id,
+					userId: me.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyLiked);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index 12ec5855d3..8582e98f76 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private downloadService: DownloadService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const users = await this.usersRepository.findOneBy({ id: me.id });
-			if (users === null) throw new ApiError(meta.errors.noSuchUser);
+			const userExist = await this.usersRepository.exist({ 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);
 			if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
diff --git a/packages/backend/src/server/api/endpoints/i/read-announcement.ts b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
index b8922b91e5..352fe54c5d 100644
--- a/packages/backend/src/server/api/endpoints/i/read-announcement.ts
+++ b/packages/backend/src/server/api/endpoints/i/read-announcement.ts
@@ -47,19 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			// Check if announcement exists
-			const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId });
+			const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
 
-			if (announcement == null) {
+			if (!announcementExist) {
 				throw new ApiError(meta.errors.noSuchAnnouncement);
 			}
 
 			// Check if already read
-			const read = await this.announcementReadsRepository.findOneBy({
-				announcementId: ps.announcementId,
-				userId: me.id,
+			const alreadyRead = await this.announcementReadsRepository.exist({
+				where: {
+					announcementId: ps.announcementId,
+					userId: me.id,
+				},
 			});
 
-			if (read != null) {
+			if (alreadyRead) {
 				return;
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/i/revoke-token.ts b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
index 93daeb0cd7..415a60147b 100644
--- a/packages/backend/src/server/api/endpoints/i/revoke-token.ts
+++ b/packages/backend/src/server/api/endpoints/i/revoke-token.ts
@@ -28,9 +28,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId });
+			const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
 
-			if (token) {
+			if (tokenExist) {
 				await this.accessTokensRepository.delete({
 					id: ps.tokenId,
 					userId: me.id,
diff --git a/packages/backend/src/server/api/endpoints/mute/create.ts b/packages/backend/src/server/api/endpoints/mute/create.ts
index ee358d5c6c..ef53f9ef41 100644
--- a/packages/backend/src/server/api/endpoints/mute/create.ts
+++ b/packages/backend/src/server/api/endpoints/mute/create.ts
@@ -79,12 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			// Check if already muting
-			const exist = await this.mutingsRepository.findOneBy({
-				muterId: muter.id,
-				muteeId: mutee.id,
+			const exist = await this.mutingsRepository.exist({
+				where: {
+					muterId: muter.id,
+					muteeId: mutee.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyMuting);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 96be5ed844..739316997a 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -217,11 +217,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 				// Check blocking
 				if (renote.userId !== me.id) {
-					const block = await this.blockingsRepository.findOneBy({
-						blockerId: renote.userId,
-						blockeeId: me.id,
+					const blockExist = await this.blockingsRepository.exist({
+						where: {
+							blockerId: renote.userId,
+							blockeeId: me.id,
+						},
 					});
-					if (block) {
+					if (blockExist) {
 						throw new ApiError(meta.errors.youHaveBeenBlocked);
 					}
 				}
@@ -240,11 +242,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 				// Check blocking
 				if (reply.userId !== me.id) {
-					const block = await this.blockingsRepository.findOneBy({
-						blockerId: reply.userId,
-						blockeeId: me.id,
+					const blockExist = await this.blockingsRepository.exist({
+						where: {
+							blockerId: reply.userId,
+							blockeeId: me.id,
+						},
 					});
-					if (block) {
+					if (blockExist) {
 						throw new ApiError(meta.errors.youHaveBeenBlocked);
 					}
 				}
diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
index 611ea19560..9299d66039 100644
--- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts
@@ -63,12 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			// if already favorited
-			const exist = await this.noteFavoritesRepository.findOneBy({
-				noteId: note.id,
-				userId: me.id,
+			const exist = await this.noteFavoritesRepository.exist({
+				where: {
+					noteId: note.id,
+					userId: me.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyFavorited);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/pages/like.ts b/packages/backend/src/server/api/endpoints/pages/like.ts
index 543c126d9c..bc66488103 100644
--- a/packages/backend/src/server/api/endpoints/pages/like.ts
+++ b/packages/backend/src/server/api/endpoints/pages/like.ts
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 
 			// if already liked
-			const exist = await this.pageLikesRepository.findOneBy({
-				pageId: page.id,
-				userId: me.id,
+			const exist = await this.pageLikesRepository.exist({
+				where: {
+					pageId: page.id,
+					userId: me.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyLiked);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/promo/read.ts b/packages/backend/src/server/api/endpoints/promo/read.ts
index 90febdbce7..9baa930f5f 100644
--- a/packages/backend/src/server/api/endpoints/promo/read.ts
+++ b/packages/backend/src/server/api/endpoints/promo/read.ts
@@ -44,12 +44,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw err;
 			});
 
-			const exist = await this.promoReadsRepository.findOneBy({
-				noteId: note.id,
-				userId: me.id,
+			const exist = await this.promoReadsRepository.exist({
+				where: {
+					noteId: note.id,
+					userId: me.id,
+				},
 			});
 
-			if (exist != null) {
+			if (exist) {
 				return;
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts
index c6f71474bc..18d66500ab 100644
--- a/packages/backend/src/server/api/endpoints/users/followers.ts
+++ b/packages/backend/src/server/api/endpoints/users/followers.ts
@@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				if (me == null) {
 					throw new ApiError(meta.errors.forbidden);
 				} else if (me.id !== user.id) {
-					const following = await this.followingsRepository.findOneBy({
-						followeeId: user.id,
-						followerId: me.id,
+					const isFollowing = await this.followingsRepository.exist({
+						where: {
+							followeeId: user.id,
+							followerId: me.id,
+						},
 					});
-					if (following == null) {
+					if (!isFollowing) {
 						throw new ApiError(meta.errors.forbidden);
 					}
 				}
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index b072c96626..6ea7b923d6 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				if (me == null) {
 					throw new ApiError(meta.errors.forbidden);
 				} else if (me.id !== user.id) {
-					const following = await this.followingsRepository.findOneBy({
-						followeeId: user.id,
-						followerId: me.id,
+					const isFollowing = await this.followingsRepository.exist({
+						where: {
+							followeeId: user.id,
+							followerId: me.id,
+						},
 					});
-					if (following == null) {
+					if (!isFollowing) {
 						throw new ApiError(meta.errors.forbidden);
 					}
 				}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
index cccffcdad4..beb0ba85ff 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts
@@ -84,11 +84,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const list = await this.userListsRepository.findOneBy({
-				id: ps.listId,
-				isPublic: true,
+			const listExist = await this.userListsRepository.exist({
+				where: {
+					id: ps.listId,
+					isPublic: true,
+				},
 			});
-			if (list === null) throw new ApiError(meta.errors.noSuchList);
+			if (!listExist) throw new ApiError(meta.errors.noSuchList);
 			const currentCount = await this.userListsRepository.countBy({
 				userId: me.id,
 			});
@@ -114,18 +116,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				});
 
 				if (currentUser.id !== me.id) {
-					const block = await this.blockingsRepository.findOneBy({
-						blockerId: currentUser.id,
-						blockeeId: me.id,
+					const blockExist = await this.blockingsRepository.exist({
+						where: {
+							blockerId: currentUser.id,
+							blockeeId: me.id,
+						},
 					});
-					if (block) {
+					if (blockExist) {
 						throw new ApiError(meta.errors.youHaveBeenBlocked);
 					}
 				}
 
-				const exist = await this.userListJoiningsRepository.findOneBy({
-					userListId: userList.id,
-					userId: currentUser.id,
+				const exist = await this.userListJoiningsRepository.exist({
+					where: {
+						userListId: userList.id,
+						userId: currentUser.id,
+					},
 				});
 
 				if (exist) {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
index ea1a022bec..2c09a47fef 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/favorite.ts
@@ -41,21 +41,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private idService: IdService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const userList = await this.userListsRepository.findOneBy({
-				id: ps.listId,
-				isPublic: true,
+			const userListExist = await this.userListsRepository.exist({
+				where: {
+					id: ps.listId,
+					isPublic: true,
+				},
 			});
 
-			if (userList === null) {
+			if (!userListExist) {
 				throw new ApiError(meta.errors.noSuchList);
 			}
 
-			const exist = await this.userListFavoritesRepository.findOneBy({
-				userId: me.id,
-				userListId: ps.listId,
+			const exist = await this.userListFavoritesRepository.exist({
+				where: {
+					userId: me.id,
+					userListId: ps.listId,
+				},
 			});
 
-			if (exist !== null) {
+			if (exist) {
 				throw new ApiError(meta.errors.alreadyFavorited);
 			}
 
diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts
index 925037e484..6e1f6b2c62 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/push.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts
@@ -100,18 +100,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 			// Check blocking
 			if (user.id !== me.id) {
-				const block = await this.blockingsRepository.findOneBy({
-					blockerId: user.id,
-					blockeeId: me.id,
+				const blockExist = await this.blockingsRepository.exist({
+					where: {
+						blockerId: user.id,
+						blockeeId: me.id,
+					},
 				});
-				if (block) {
+				if (blockExist) {
 					throw new ApiError(meta.errors.youHaveBeenBlocked);
 				}
 			}
 
-			const exist = await this.userListJoiningsRepository.findOneBy({
-				userListId: userList.id,
-				userId: user.id,
+			const exist = await this.userListJoiningsRepository.exist({
+				where: {
+					userListId: userList.id,
+					userId: user.id,
+				},
 			});
 
 			if (exist) {
diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts
index 8077841c8c..3fd418d04e 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts
@@ -69,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 					userListId: ps.listId,
 				});
 				if (me !== null) {
-					additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
-						userId: me.id,
-						userListId: ps.listId,
-					}) !== null);
+					additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
+						where: {
+							userId: me.id,
+							userListId: ps.listId,
+						},
+					});
 				} else {
 					additionalProperties.isLiked = false;
 				}
diff --git a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
index be8e317816..a7c3b58947 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/unfavorite.ts
@@ -39,12 +39,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private userListFavoritesRepository: UserListFavoritesRepository,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const userList = await this.userListsRepository.findOneBy({
-				id: ps.listId,
-				isPublic: true,
+			const userListExist = await this.userListsRepository.exist({
+				where: {
+					id: ps.listId,
+					isPublic: true,
+				},
 			});
 
-			if (userList === null) {
+			if (!userListExist) {
 				throw new ApiError(meta.errors.noSuchList);
 			}
 
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index d97d2b8ba9..ea4cff0bc0 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -34,11 +34,13 @@ class UserListChannel extends Channel {
 		this.listId = params.listId as string;
 
 		// Check existence and owner
-		const list = await this.userListsRepository.findOneBy({
-			id: this.listId,
-			userId: this.user!.id,
+		const listExist = await this.userListsRepository.exist({
+			where: {
+				id: this.listId,
+				userId: this.user!.id,
+			},
 		});
-		if (!list) return;
+		if (!listExist) return;
 
 		// Subscribe stream
 		this.subscriber.on(`userListStream:${this.listId}`, this.send);
-- 
GitLab


From 9845ccec5b70501b36e05a84206e38a1f53957c4 Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Tue, 11 Jul 2023 18:24:10 +0900
Subject: [PATCH 15/17] =?UTF-8?q?=E3=82=AA=E3=83=95=E3=83=A9=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E6=99=82=E3=81=AE=E7=94=BB=E9=9D=A2=E3=81=AB=E3=83=AA?=
 =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E3=83=9C=E3=82=BF=E3=83=B3=E3=82=92?=
 =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20=20(#11242)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat: オフライン時の画面にリロードボタンを追加

リロードのためのボタンがないとPWAでインターネットが復帰しても何もできなくなるため。

* docs(changelog): add オフライン時の画面にリロードボタンを追加
---
 CHANGELOG.md          |  1 +
 packages/sw/src/sw.ts | 13 ++++++++++++-
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a03ca4ab86..8d6dd3a7db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@
 - ユーザーのContextMenuに「アンテナに追加」ボタンを追加
 - フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
 - 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
+- オフライン時の画面にリロードボタンを追加
 - Fix: サーバーメトリクスが90度傾いている
 - Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
 - Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts
index 2783f2bfb5..1f085c7392 100644
--- a/packages/sw/src/sw.ts
+++ b/packages/sw/src/sw.ts
@@ -21,6 +21,10 @@ globalThis.addEventListener('activate', ev => {
 	);
 });
 
+function offlineContentHTML(): string {
+	return `<!doctype html>Offline. Service Worker @${_VERSION_} <button onclick="location.reload()">reload</button>`
+}
+
 globalThis.addEventListener('fetch', ev => {
 	let isHTMLRequest = false;
 	if (ev.request.headers.get('sec-fetch-dest') === 'document') {
@@ -34,7 +38,14 @@ globalThis.addEventListener('fetch', ev => {
 	if (!isHTMLRequest) return;
 	ev.respondWith(
 		fetch(ev.request)
-			.catch(() => new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 })),
+			.catch(() => {
+				return new Response(offlineContentHTML(), {
+					status: 200,
+					headers: {
+						'content-type': 'text/html',
+					},
+				});
+			}),
 	);
 });
 
-- 
GitLab


From b97694b083aca7df07d0763202aab4cd1ff33c01 Mon Sep 17 00:00:00 2001
From: Lui <injabie3@gmail.com>
Date: Wed, 12 Jul 2023 00:31:48 -0700
Subject: [PATCH 16/17] fix: typo in custom emojis manager (#11250)

---
 packages/frontend/src/pages/custom-emojis-manager.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index dde4d67f18..359bbeadc3 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -18,7 +18,7 @@
 						<MkButton inline @click="setTagBulk">Set tag</MkButton>
 						<MkButton inline @click="addTagBulk">Add tag</MkButton>
 						<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
-						<MkButton inline @click="setLisenceBulk">Set Lisence</MkButton>
+						<MkButton inline @click="setLicenseBulk">Set License</MkButton>
 						<MkButton inline danger @click="delBulk">Delete</MkButton>
 					</div>
 					<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
@@ -221,7 +221,7 @@ const setCategoryBulk = async () => {
 	emojisPaginationComponent.value.reload();
 };
 
-const setLisenceBulk = async () => {
+const setLicenseBulk = async () => {
 	const { canceled, result } = await os.inputText({
 		title: 'License',
 	});
-- 
GitLab


From cd9affd568d3f7c02398630bbb0d19a69a9651aa Mon Sep 17 00:00:00 2001
From: Kagami Sascha Rosylight <saschanaz@outlook.com>
Date: Wed, 12 Jul 2023 22:27:51 +0200
Subject: [PATCH 17/17] fix(locales, storybook): use default import (#11259)

---
 locales/index.d.ts                             | 2 +-
 packages/frontend/.storybook/preload-locale.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 8697b22e21..7555984b24 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2158,4 +2158,4 @@ export interface Locale {
 declare const locales: {
     [lang: string]: Locale;
 };
-export = locales;
+export default locales;
diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts
index 636931967f..2b7362b88d 100644
--- a/packages/frontend/.storybook/preload-locale.ts
+++ b/packages/frontend/.storybook/preload-locale.ts
@@ -1,5 +1,5 @@
 import { writeFile } from 'node:fs/promises';
-import * as locales from '../../../locales/index.js';
+import locales from '../../../locales/index.js';
 
 await writeFile(
 	new URL('locale.ts', import.meta.url),
-- 
GitLab