From 453574644c9d8fd19b3f11adc8b24250c00ea268 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Sun, 26 Mar 2023 09:25:51 +0000
Subject: [PATCH 01/11] fix: add workaround for cat ears on iOS Safari

---
 packages/frontend/src/components/global/MkPageHeader.vue | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 4d968db6a..013dc2fd6 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -8,7 +8,9 @@
 
 		<template v-if="metadata">
 			<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
-				<MkAvatar v-if="metadata.avatar" :class="$style.titleAvatar" :user="metadata.avatar" indicator/>
+				<div v-if="metadata.avatar" :class="$style.titleAvatarContainer">
+					<MkAvatar :class="$style.titleAvatar" :user="metadata.avatar" indicator/>
+				</div>
 				<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
 
 				<div :class="$style.title">
@@ -249,6 +251,11 @@ onUnmounted(() => {
 	margin-left: 24px;
 }
 
+.titleAvatarContainer {
+	overflow: hidden;
+	padding: 8px 0;
+}
+
 .titleAvatar {
 	$size: 32px;
 	display: inline-block;

From 07cfab04890a46159e78ec0199634f98ff9f6ddd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Sun, 26 Mar 2023 09:36:46 +0000
Subject: [PATCH 02/11] fix: add tight state to cat ears for workarounds

---
 .../frontend/src/components/global/MkAvatar.vue    | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 814ab53d2..ed104cb0f 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -86,6 +86,18 @@ watch(() => props.user.avatarBlurhash, () => {
 	to { transform: rotate(-37.6deg) skew(-30deg); }
 }
 
+@keyframes eartightleft {
+	from { transform: rotate(37.6deg) skew(30deg); }
+	50% { transform: rotate(37.4deg) skew(30deg); }
+	to { transform: rotate(37.6deg) skew(30deg); }
+}
+
+@keyframes eartightright {
+	from { transform: rotate(-37.6deg) skew(-30deg); }
+	50% { transform: rotate(-37.4deg) skew(-30deg); }
+	to { transform: rotate(-37.6deg) skew(-30deg); }
+}
+
 .root {
 	position: relative;
 	display: inline-block;
@@ -184,6 +196,7 @@ watch(() => props.user.avatarBlurhash, () => {
 
 		> .earLeft {
 			transform: rotate(37.5deg) skew(30deg);
+			animation: eartightleft 6s infinite;
 
 			&, &::after {
 				border-radius: 0 75% 75%;
@@ -205,6 +218,7 @@ watch(() => props.user.avatarBlurhash, () => {
 
 		> .earRight {
 			transform: rotate(-37.5deg) skew(-30deg);
+			animation: eartightright 6s infinite;
 
 			&, &::after {
 				border-radius: 75% 0 75% 75%;

From 4d3f0be419a972a9233e187458ff45f13baf1dde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Mon, 27 Mar 2023 05:16:39 +0000
Subject: [PATCH 03/11] chore: respect the animation preference

---
 .../frontend/src/components/global/MkAvatar.vue   | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index ed104cb0f..14075cd7b 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -1,5 +1,5 @@
 <template>
-<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
+<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
 	<img :class="$style.inner" :src="url" decoding="async"/>
 	<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
 	<div v-if="user.isCat" :class="[$style.ears, { [$style.mask]: useBlurEffect }]">
@@ -27,6 +27,7 @@ import { acct, userPage } from '@/filters/user';
 import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
 import { defaultStore } from '@/store';
 
+const animation = $ref(defaultStore.state.animation);
 const squareAvatars = $ref(defaultStore.state.squareAvatars);
 const useBlurEffect = $ref(defaultStore.state.useBlurEffect);
 
@@ -156,6 +157,14 @@ watch(() => props.user.avatarBlurhash, () => {
 			mask:
 				url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><filter id="a"><feGaussianBlur in="SourceGraphic" stdDeviation="1"/></filter><circle cx="16" cy="16" r="15" filter="url(%23a)"/></svg>') exclude center / 50% 50%,
 				linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
+
+			> .earLeft {
+				animation: eartightright 6s infinite;
+			}
+
+			> .earRight {
+				animation: eartightleft 6s infinite;
+			}
 		}
 
 		> .earLeft,
@@ -196,7 +205,6 @@ watch(() => props.user.avatarBlurhash, () => {
 
 		> .earLeft {
 			transform: rotate(37.5deg) skew(30deg);
-			animation: eartightleft 6s infinite;
 
 			&, &::after {
 				border-radius: 0 75% 75%;
@@ -218,7 +226,6 @@ watch(() => props.user.avatarBlurhash, () => {
 
 		> .earRight {
 			transform: rotate(-37.5deg) skew(-30deg);
-			animation: eartightright 6s infinite;
 
 			&, &::after {
 				border-radius: 75% 0 75% 75%;
@@ -239,7 +246,7 @@ watch(() => props.user.avatarBlurhash, () => {
 		}
 	}
 
-	&:hover {
+	&.animation:hover {
 		> .ears {
 			> .earLeft {
 				animation: earwiggleleft 1s infinite;

From 8963e36aa200ac9f663265315223a07544a17965 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Mon, 27 Mar 2023 05:25:32 +0000
Subject: [PATCH 04/11] docs: note change logs

---
 CHANGELOG.md | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8fa82ad2..fdb88c017 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,13 @@
 -
 
 ### Client
--
+- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
+  - 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
+	  - 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
+		- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
+		  - 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
+			- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
+	- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
 
 ### Server
 -

From 49d4c538fee8a44796b26cd987b953e2cc770c0d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Wed, 29 Mar 2023 23:37:43 +0900
Subject: [PATCH 05/11] fix: typo

---
 packages/frontend/src/components/global/MkAvatar.vue | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 14075cd7b..9a21941c8 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -159,11 +159,11 @@ watch(() => props.user.avatarBlurhash, () => {
 				linear-gradient(#fff, #fff); // polyfill of `image(#fff)`
 
 			> .earLeft {
-				animation: eartightright 6s infinite;
+				animation: eartightleft 6s infinite;
 			}
 
 			> .earRight {
-				animation: eartightleft 6s infinite;
+				animation: eartightright 6s infinite;
 			}
 		}
 

From 14977ed2b703aaeafb26fd811035ed8fa7166ee0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Fri, 31 Mar 2023 07:47:03 +0000
Subject: [PATCH 06/11] fix: the avatar in the title bar is clipped

---
 .../src/components/global/MkPageHeader.vue      | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 31f23486d..710edd797 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -252,17 +252,18 @@ onUnmounted(() => {
 }
 
 .titleAvatarContainer {
-	overflow: hidden;
-	padding: 8px 0;
+	$size: 32px;
+	contain: strict;
+	overflow: clip;
+	width: $size;
+	height: $size;
+	padding: 8px;
+	flex-shrink: 0;
 }
 
 .titleAvatar {
-	$size: 32px;
-	display: inline-block;
-	width: $size;
-	height: $size;
-	vertical-align: bottom;
-	margin: 0 8px;
+	width: 100%;
+	height: 100%;
 	pointer-events: none;
 }
 

From 4f9f625e6574990dfcd736c7a7d059af8fef7234 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 3 Apr 2023 11:49:58 +0900
Subject: [PATCH 07/11] perf(backend): cache timeline of a channel to redis

---
 packages/backend/src/core/IdService.ts        | 15 +++++++++-
 .../backend/src/core/NoteCreateService.ts     |  8 +++++
 packages/backend/src/misc/id/aid.ts           |  5 ++++
 .../server/api/endpoints/channels/timeline.ts | 29 +++++++++++++++++--
 4 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts
index 31c0819e5..94084ad84 100644
--- a/packages/backend/src/core/IdService.ts
+++ b/packages/backend/src/core/IdService.ts
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
 import { ulid } from 'ulid';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
-import { genAid } from '@/misc/id/aid.js';
+import { genAid, parseAid } from '@/misc/id/aid.js';
 import { genMeid } from '@/misc/id/meid.js';
 import { genMeidg } from '@/misc/id/meidg.js';
 import { genObjectId } from '@/misc/id/object-id.js';
@@ -32,4 +32,17 @@ export class IdService {
 			default: throw new Error('unrecognized id generation method');
 		}
 	}
+
+	@bindThis
+	public parse(id: string): { date: Date; } {
+		switch (this.method) {
+			case 'aid': return parseAid(id);
+			// TODO
+			//case 'meid':
+			//case 'meidg':
+			//case 'ulid':
+			//case 'objectid':
+			default: throw new Error('unrecognized id generation method');
+		}
+	}
 }
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 7d0805376..93fab9d17 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -1,6 +1,7 @@
 import { setImmediate } from 'node:timers/promises';
 import * as mfm from 'mfm-js';
 import { In, DataSource } from 'typeorm';
+import Redis from 'ioredis';
 import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 import { extractMentions } from '@/misc/extract-mentions.js';
 import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
@@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 		@Inject(DI.db)
 		private db: DataSource,
 
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+
 		@Inject(DI.usersRepository)
 		private usersRepository: UsersRepository,
 
@@ -321,6 +325,10 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 		const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
 
+		if (data.channel) {
+			this.redisClient.xadd(`channelTimeline:${data.channel.id}`, 'MAXLEN', '~', '1000', `${this.idService.parse(note.id).date.getTime()}-*`, 'note', note.id);
+		}
+
 		setImmediate('post created', { signal: this.#shutdownController.signal }).then(
 			() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
 			() => { /* aborted, ignore this */ },
diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts
index 19c8546f9..93a9929aa 100644
--- a/packages/backend/src/misc/id/aid.ts
+++ b/packages/backend/src/misc/id/aid.ts
@@ -23,3 +23,8 @@ export function genAid(date: Date): string {
 	counter++;
 	return getTime(t) + getNoise();
 }
+
+export function parseAid(id: string): { date: Date; } {
+	const time = parseInt(id.slice(0, 8), 36) + TIME2000;
+	return { date: new Date(time) };
+}
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index cdaa40013..eef343d13 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -1,10 +1,12 @@
 import { Inject, Injectable } from '@nestjs/common';
+import Redis from 'ioredis';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { ChannelsRepository, NotesRepository } from '@/models/index.js';
 import { QueryService } from '@/core/QueryService.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 import ActiveUsersChart from '@/core/chart/charts/active-users.js';
 import { DI } from '@/di-symbols.js';
+import { IdService } from '@/core/IdService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -48,12 +50,16 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> {
 	constructor(
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
 		@Inject(DI.channelsRepository)
 		private channelsRepository: ChannelsRepository,
 
+		private idService: IdService,
 		private noteEntityService: NoteEntityService,
 		private queryService: QueryService,
 		private activeUsersChart: ActiveUsersChart,
@@ -67,9 +73,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw new ApiError(meta.errors.noSuchChannel);
 			}
 
+			const noteIdsRes = await this.redisClient.xrevrange(
+				`channelTimeline:${channel.id}`,
+				ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
+				'-',
+				'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
+
+			if (noteIdsRes.length === 0) {
+				return [];
+			}
+
+			const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
+
+			if (noteIds.length === 0) {
+				return [];
+			}
+
 			//#region Construct query
-			const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-				.andWhere('note.channelId = :channelId', { channelId: channel.id })
+			const query = this.notesRepository.createQueryBuilder('note')
+				.where('note.id IN (:...noteIds)', { noteIds: noteIds })
 				.innerJoinAndSelect('note.user', 'user')
 				.leftJoinAndSelect('user.avatar', 'avatar')
 				.leftJoinAndSelect('user.banner', 'banner')
@@ -90,7 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 			//#endregion
 
-			const timeline = await query.take(ps.limit).getMany();
+			const timeline = await query.getMany();
+			timeline.sort((a, b) => a.id > b.id ? -1 : 1);
 
 			if (me) this.activeUsersChart.read(me);
 

From c032dd1214df6f7640e0ee2a39d25796e622a1b2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 3 Apr 2023 11:50:17 +0900
Subject: [PATCH 08/11] fix(frontend): tweak MkPagination behaviouyr

---
 .../frontend/src/components/MkPagination.vue  | 52 +++++++++----------
 1 file changed, 25 insertions(+), 27 deletions(-)

diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 867d43257..cd8af560e 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -163,21 +163,22 @@ async function init(): Promise<void> {
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 	await os.api(props.pagination.endpoint, {
 		...params,
-		limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
+		limit: props.pagination.limit ?? 10,
 	}).then(res => {
 		for (let i = 0; i < res.length; i++) {
 			const item = res[i];
 			if (i === 3) item._shouldInsertAd_ = true;
 		}
-		if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
-			res.pop();
+
+		if (res.length === 0 || props.pagination.noPaging) {
+			items.value = res;
+			more.value = false;
+		} else {
 			if (props.pagination.reversed) moreFetching.value = true;
 			items.value = res;
 			more.value = true;
-		} else {
-			items.value = res;
-			more.value = false;
 		}
+
 		offset.value = res.length;
 		error.value = false;
 		fetching.value = false;
@@ -198,7 +199,7 @@ const fetchMore = async (): Promise<void> => {
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 	await os.api(props.pagination.endpoint, {
 		...params,
-		limit: SECOND_FETCH_LIMIT + 1,
+		limit: SECOND_FETCH_LIMIT,
 		...(props.pagination.offsetMode ? {
 			offset: offset.value,
 		} : {
@@ -227,20 +228,7 @@ const fetchMore = async (): Promise<void> => {
 			});
 		};
 
-		if (res.length > SECOND_FETCH_LIMIT) {
-			res.pop();
-
-			if (props.pagination.reversed) {
-				reverseConcat(res).then(() => {
-					more.value = true;
-					moreFetching.value = false;
-				});
-			} else {
-				items.value = items.value.concat(res);
-				more.value = true;
-				moreFetching.value = false;
-			}
-		} else {
+		if (res.length === 0) {
 			if (props.pagination.reversed) {
 				reverseConcat(res).then(() => {
 					more.value = false;
@@ -251,6 +239,17 @@ const fetchMore = async (): Promise<void> => {
 				more.value = false;
 				moreFetching.value = false;
 			}
+		} else {
+			if (props.pagination.reversed) {
+				reverseConcat(res).then(() => {
+					more.value = true;
+					moreFetching.value = false;
+				});
+			} else {
+				items.value = items.value.concat(res);
+				more.value = true;
+				moreFetching.value = false;
+			}
 		}
 		offset.value += res.length;
 	}, err => {
@@ -264,20 +263,19 @@ const fetchMoreAhead = async (): Promise<void> => {
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
 	await os.api(props.pagination.endpoint, {
 		...params,
-		limit: SECOND_FETCH_LIMIT + 1,
+		limit: SECOND_FETCH_LIMIT,
 		...(props.pagination.offsetMode ? {
 			offset: offset.value,
 		} : {
 			sinceId: items.value[items.value.length - 1].id,
 		}),
 	}).then(res => {
-		if (res.length > SECOND_FETCH_LIMIT) {
-			res.pop();
-			items.value = items.value.concat(res);
-			more.value = true;
-		} else {
+		if (res.length === 0) {
 			items.value = items.value.concat(res);
 			more.value = false;
+		} else {
+			items.value = items.value.concat(res);
+			more.value = true;
 		}
 		offset.value += res.length;
 		moreFetching.value = false;

From b53d6c7f8ca1a712eab44967e8d05a0cc7bcc034 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 3 Apr 2023 12:11:16 +0900
Subject: [PATCH 09/11] perf(backend): store notes of an antenna to redis
 instead of postgresql

Resolve #10169
---
 CHANGELOG.md                                  |  2 +
 .../migration/1680491187535-cleanup.js        | 10 +++
 packages/backend/src/core/AntennaService.ts   | 61 +++----------------
 .../backend/src/core/NoteCreateService.ts     |  6 +-
 packages/backend/src/core/NoteReadService.ts  | 43 +------------
 .../src/core/entities/AntennaEntityService.ts |  9 +--
 .../src/core/entities/UserEntityService.ts    |  8 +--
 packages/backend/src/di-symbols.ts            |  1 -
 .../backend/src/models/RepositoryModule.ts    | 10 +--
 .../src/models/entities/AntennaNote.ts        | 43 -------------
 packages/backend/src/models/index.ts          |  3 -
 packages/backend/src/postgres.ts              |  2 -
 .../queue/processors/CleanProcessorService.ts |  5 +-
 .../server/api/endpoints/antennas/notes.ts    | 40 ++++++++----
 14 files changed, 64 insertions(+), 179 deletions(-)
 create mode 100644 packages/backend/migration/1680491187535-cleanup.js
 delete mode 100644 packages/backend/src/models/entities/AntennaNote.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 711d9db62..5f3501735 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@
 ### General
 - チャンネルをお気に入りに登録できるように
 - チャンネルにノートをピン留めできるように
+- アンテナのタイムライン取得時のパフォーマンスを向上
+- チャンネルのタイムライン取得時のパフォーマンスを向上
 
 ### Client
 - 検索ページでURLを入力した際に照会したときと同等の挙動をするように
diff --git a/packages/backend/migration/1680491187535-cleanup.js b/packages/backend/migration/1680491187535-cleanup.js
new file mode 100644
index 000000000..1e609ca06
--- /dev/null
+++ b/packages/backend/migration/1680491187535-cleanup.js
@@ -0,0 +1,10 @@
+export class cleanup1680491187535 {
+    name = 'cleanup1680491187535'
+
+    async up(queryRunner) {
+        await queryRunner.query(`DROP TABLE "antenna_note" `);
+    }
+
+    async down(queryRunner) {
+    }
+}
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index aaa26a832..4bd3f39af 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
 import * as Acct from '@/misc/acct.js';
 import type { Packed } from '@/misc/json-schema.js';
 import { DI } from '@/di-symbols.js';
-import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
+import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
 import { UtilityService } from '@/core/UtilityService.js';
 import { bindThis } from '@/decorators.js';
 import { StreamMessages } from '@/server/api/stream/types.js';
@@ -24,6 +24,9 @@ export class AntennaService implements OnApplicationShutdown {
 	private antennas: Antenna[];
 
 	constructor(
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+
 		@Inject(DI.redisSubscriber)
 		private redisSubscriber: Redis.Redis,
 
@@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
-		@Inject(DI.antennaNotesRepository)
-		private antennaNotesRepository: AntennaNotesRepository,
-
 		@Inject(DI.antennasRepository)
 		private antennasRepository: AntennasRepository,
 
@@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
 
 	@bindThis
 	public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
-		// 通知しない設定になっているか、自分自身の投稿なら既読にする
-		const read = !antenna.notify || (antenna.userId === noteUser.id);
-	
-		this.antennaNotesRepository.insert({
-			id: this.idService.genId(),
-			antennaId: antenna.id,
-			noteId: note.id,
-			read: read,
-		});
-	
+		this.redisClient.xadd(
+			`antennaTimeline:${antenna.id}`,
+			'MAXLEN', '~', '200',
+			`${this.idService.parse(note.id).date.getTime()}-*`,
+			'note', note.id);
+		
 		this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
-	
-		if (!read) {
-			const mutings = await this.mutingsRepository.find({
-				where: {
-					muterId: antenna.userId,
-				},
-				select: ['muteeId'],
-			});
-	
-			// Copy
-			const _note: Note = {
-				...note,
-			};
-	
-			if (note.replyId != null) {
-				_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
-			}
-			if (note.renoteId != null) {
-				_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
-			}
-	
-			if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
-				return;
-			}
-	
-			// 2秒経っても既読にならなかったら通知
-			setTimeout(async () => {
-				const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
-				if (unread) {
-					this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
-					this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
-						antenna: { id: antenna.id, name: antenna.name },
-						note: await this.noteEntityService.pack(note),
-					});
-				}
-			}, 2000);
-		}
 	}
 
 	// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 93fab9d17..7af709943 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -326,7 +326,11 @@ export class NoteCreateService implements OnApplicationShutdown {
 		const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
 
 		if (data.channel) {
-			this.redisClient.xadd(`channelTimeline:${data.channel.id}`, 'MAXLEN', '~', '1000', `${this.idService.parse(note.id).date.getTime()}-*`, 'note', note.id);
+			this.redisClient.xadd(
+				`channelTimeline:${data.channel.id}`,
+				'MAXLEN', '~', '1000',
+				`${this.idService.parse(note.id).date.getTime()}-*`,
+				'note', note.id);
 		}
 
 		setImmediate('post created', { signal: this.#shutdownController.signal }).then(
diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts
index 22d72815e..1bf0eb918 100644
--- a/packages/backend/src/core/NoteReadService.ts
+++ b/packages/backend/src/core/NoteReadService.ts
@@ -8,7 +8,7 @@ import type { Packed } from '@/misc/json-schema.js';
 import type { Note } from '@/models/entities/Note.js';
 import { IdService } from '@/core/IdService.js';
 import { GlobalEventService } from '@/core/GlobalEventService.js';
-import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
+import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { bindThis } from '@/decorators.js';
 import { NotificationService } from './NotificationService.js';
@@ -38,9 +38,6 @@ export class NoteReadService implements OnApplicationShutdown {
 		@Inject(DI.channelFollowingsRepository)
 		private channelFollowingsRepository: ChannelFollowingsRepository,
 
-		@Inject(DI.antennaNotesRepository)
-		private antennaNotesRepository: AntennaNotesRepository,
-
 		private userEntityService: UserEntityService,
 		private idService: IdService,
 		private globalEventService: GlobalEventService,
@@ -121,7 +118,6 @@ export class NoteReadService implements OnApplicationShutdown {
 		const readMentions: (Note | Packed<'Note'>)[] = [];
 		const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
 		const readChannelNotes: (Note | Packed<'Note'>)[] = [];
-		const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
 
 		for (const note of notes) {
 			if (note.mentions && note.mentions.includes(userId)) {
@@ -133,14 +129,6 @@ export class NoteReadService implements OnApplicationShutdown {
 			if (note.channelId && followingChannels.has(note.channelId)) {
 				readChannelNotes.push(note);
 			}
-
-			if (note.user != null) { // たぶんnullになることは無いはずだけど一応
-				for (const antenna of myAntennas) {
-					if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
-						readAntennaNotes.push(note);
-					}
-				}
-			}
 		}
 
 		if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
@@ -186,35 +174,6 @@ export class NoteReadService implements OnApplicationShutdown {
 				noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
 			});
 		}
-
-		if (readAntennaNotes.length > 0) {
-			await this.antennaNotesRepository.update({
-				antennaId: In(myAntennas.map(a => a.id)),
-				noteId: In(readAntennaNotes.map(n => n.id)),
-			}, {
-				read: true,
-			});
-
-			// TODO: まとめてクエリしたい
-			for (const antenna of myAntennas) {
-				const count = await this.antennaNotesRepository.countBy({
-					antennaId: antenna.id,
-					read: false,
-				});
-
-				if (count === 0) {
-					this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
-					this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
-				}
-			}
-	
-			this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
-				if (!unread) {
-					this.globalEventService.publishMainStream(userId, 'readAllAntennas');
-					this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
-				}
-			});
-		}
 	}
 
 	onApplicationShutdown(signal?: string | undefined): void {
diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts
index e02daefd6..328511f5d 100644
--- a/packages/backend/src/core/entities/AntennaEntityService.ts
+++ b/packages/backend/src/core/entities/AntennaEntityService.ts
@@ -1,6 +1,6 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
+import type { AntennasRepository } from '@/models/index.js';
 import type { Packed } from '@/misc/json-schema.js';
 import type { Antenna } from '@/models/entities/Antenna.js';
 import { bindThis } from '@/decorators.js';
@@ -10,9 +10,6 @@ export class AntennaEntityService {
 	constructor(
 		@Inject(DI.antennasRepository)
 		private antennasRepository: AntennasRepository,
-
-		@Inject(DI.antennaNotesRepository)
-		private antennaNotesRepository: AntennaNotesRepository,
 	) {
 	}
 
@@ -22,8 +19,6 @@ export class AntennaEntityService {
 	): Promise<Packed<'Antenna'>> {
 		const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
 
-		const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
-
 		return {
 			id: antenna.id,
 			createdAt: antenna.createdAt.toISOString(),
@@ -38,7 +33,7 @@ export class AntennaEntityService {
 			withReplies: antenna.withReplies,
 			withFile: antenna.withFile,
 			isActive: antenna.isActive,
-			hasUnreadNote,
+			hasUnreadNote: false, // TODO
 		};
 	}
 }
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b693883e0..61fd6f2f6 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -12,7 +12,7 @@ import { KVCache } from '@/misc/cache.js';
 import type { Instance } from '@/models/entities/Instance.js';
 import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
 import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
+import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
 import { bindThis } from '@/decorators.js';
 import { RoleService } from '@/core/RoleService.js';
 import type { OnModuleInit } from '@nestjs/common';
@@ -108,9 +108,6 @@ export class UserEntityService implements OnModuleInit {
 		@Inject(DI.announcementsRepository)
 		private announcementsRepository: AnnouncementsRepository,
 
-		@Inject(DI.antennaNotesRepository)
-		private antennaNotesRepository: AntennaNotesRepository,
-
 		@Inject(DI.pagesRepository)
 		private pagesRepository: PagesRepository,
 
@@ -223,6 +220,7 @@ export class UserEntityService implements OnModuleInit {
 
 	@bindThis
 	public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
+		/*
 		const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
 
 		const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
@@ -231,6 +229,8 @@ export class UserEntityService implements OnModuleInit {
 		}) : null;
 
 		return unread != null;
+		*/
+		return false; // TODO
 	}
 
 	@bindThis
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 4f475a03a..f2ab6cb86 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -54,7 +54,6 @@ export const DI = {
 	clipNotesRepository: Symbol('clipNotesRepository'),
 	clipFavoritesRepository: Symbol('clipFavoritesRepository'),
 	antennasRepository: Symbol('antennasRepository'),
-	antennaNotesRepository: Symbol('antennaNotesRepository'),
 	promoNotesRepository: Symbol('promoNotesRepository'),
 	promoReadsRepository: Symbol('promoReadsRepository'),
 	relaysRepository: Symbol('relaysRepository'),
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index da7faf9ff..b74ee3689 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
 import { Module } from '@nestjs/common';
 import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
 import type { DataSource } from 'typeorm';
 import type { Provider } from '@nestjs/common';
 
@@ -298,12 +298,6 @@ const $antennasRepository: Provider = {
 	inject: [DI.db],
 };
 
-const $antennaNotesRepository: Provider = {
-	provide: DI.antennaNotesRepository,
-	useFactory: (db: DataSource) => db.getRepository(AntennaNote),
-	inject: [DI.db],
-};
-
 const $promoNotesRepository: Provider = {
 	provide: DI.promoNotesRepository,
 	useFactory: (db: DataSource) => db.getRepository(PromoNote),
@@ -453,7 +447,6 @@ const $roleAssignmentsRepository: Provider = {
 		$clipNotesRepository,
 		$clipFavoritesRepository,
 		$antennasRepository,
-		$antennaNotesRepository,
 		$promoNotesRepository,
 		$promoReadsRepository,
 		$relaysRepository,
@@ -521,7 +514,6 @@ const $roleAssignmentsRepository: Provider = {
 		$clipNotesRepository,
 		$clipFavoritesRepository,
 		$antennasRepository,
-		$antennaNotesRepository,
 		$promoNotesRepository,
 		$promoReadsRepository,
 		$relaysRepository,
diff --git a/packages/backend/src/models/entities/AntennaNote.ts b/packages/backend/src/models/entities/AntennaNote.ts
deleted file mode 100644
index 5524a8936..000000000
--- a/packages/backend/src/models/entities/AntennaNote.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
-import { id } from '../id.js';
-import { Note } from './Note.js';
-import { Antenna } from './Antenna.js';
-
-@Entity()
-@Index(['noteId', 'antennaId'], { unique: true })
-export class AntennaNote {
-	@PrimaryColumn(id())
-	public id: string;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The note ID.',
-	})
-	public noteId: Note['id'];
-
-	@ManyToOne(type => Note, {
-		onDelete: 'CASCADE',
-	})
-	@JoinColumn()
-	public note: Note | null;
-
-	@Index()
-	@Column({
-		...id(),
-		comment: 'The antenna ID.',
-	})
-	public antennaId: Antenna['id'];
-
-	@ManyToOne(type => Antenna, {
-		onDelete: 'CASCADE',
-	})
-	@JoinColumn()
-	public antenna: Antenna | null;
-
-	@Index()
-	@Column('boolean', {
-		default: false,
-	})
-	public read: boolean;
-}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index 79bd014ce..c4c9717ed 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -4,7 +4,6 @@ import { Ad } from '@/models/entities/Ad.js';
 import { Announcement } from '@/models/entities/Announcement.js';
 import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
 import { Antenna } from '@/models/entities/Antenna.js';
-import { AntennaNote } from '@/models/entities/AntennaNote.js';
 import { App } from '@/models/entities/App.js';
 import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
 import { AuthSession } from '@/models/entities/AuthSession.js';
@@ -73,7 +72,6 @@ export {
 	Announcement,
 	AnnouncementRead,
 	Antenna,
-	AntennaNote,
 	App,
 	AttestationChallenge,
 	AuthSession,
@@ -141,7 +139,6 @@ export type AdsRepository = Repository<Ad>;
 export type AnnouncementsRepository = Repository<Announcement>;
 export type AnnouncementReadsRepository = Repository<AnnouncementRead>;
 export type AntennasRepository = Repository<Antenna>;
-export type AntennaNotesRepository = Repository<AntennaNote>;
 export type AppsRepository = Repository<App>;
 export type AttestationChallengesRepository = Repository<AttestationChallenge>;
 export type AuthSessionsRepository = Repository<AuthSession>;
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index cbe3814a2..024aa114f 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -12,7 +12,6 @@ import { Ad } from '@/models/entities/Ad.js';
 import { Announcement } from '@/models/entities/Announcement.js';
 import { AnnouncementRead } from '@/models/entities/AnnouncementRead.js';
 import { Antenna } from '@/models/entities/Antenna.js';
-import { AntennaNote } from '@/models/entities/AntennaNote.js';
 import { App } from '@/models/entities/App.js';
 import { AttestationChallenge } from '@/models/entities/AttestationChallenge.js';
 import { AuthSession } from '@/models/entities/AuthSession.js';
@@ -168,7 +167,6 @@ export const entities = [
 	ClipNote,
 	ClipFavorite,
 	Antenna,
-	AntennaNote,
 	PromoNote,
 	PromoRead,
 	Relay,
diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts
index 9534454fd..3feb86f86 100644
--- a/packages/backend/src/queue/processors/CleanProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanProcessorService.ts
@@ -1,7 +1,7 @@
 import { Inject, Injectable } from '@nestjs/common';
 import { In, LessThan } from 'typeorm';
 import { DI } from '@/di-symbols.js';
-import type { AntennaNotesRepository, AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
+import type { AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
 import type { Config } from '@/config.js';
 import type Logger from '@/logger.js';
 import { bindThis } from '@/decorators.js';
@@ -29,9 +29,6 @@ export class CleanProcessorService {
 		@Inject(DI.antennasRepository)
 		private antennasRepository: AntennasRepository,
 
-		@Inject(DI.antennaNotesRepository)
-		private antennaNotesRepository: AntennaNotesRepository,
-
 		@Inject(DI.roleAssignmentsRepository)
 		private roleAssignmentsRepository: RoleAssignmentsRepository,
 
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index 039ba1115..364f9d9c0 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -1,10 +1,12 @@
 import { Inject, Injectable } from '@nestjs/common';
+import Redis from 'ioredis';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { NotesRepository, AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
+import type { NotesRepository, AntennasRepository } from '@/models/index.js';
 import { QueryService } from '@/core/QueryService.js';
 import { NoteReadService } from '@/core/NoteReadService.js';
 import { DI } from '@/di-symbols.js';
 import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import { IdService } from '@/core/IdService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -50,15 +52,16 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> {
 	constructor(
+		@Inject(DI.redis)
+		private redisClient: Redis.Redis,
+
 		@Inject(DI.notesRepository)
 		private notesRepository: NotesRepository,
 
 		@Inject(DI.antennasRepository)
 		private antennasRepository: AntennasRepository,
 
-		@Inject(DI.antennaNotesRepository)
-		private antennaNotesRepository: AntennaNotesRepository,
-
+		private idService: IdService,
 		private noteEntityService: NoteEntityService,
 		private queryService: QueryService,
 		private noteReadService: NoteReadService,
@@ -73,9 +76,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				throw new ApiError(meta.errors.noSuchAntenna);
 			}
 
-			const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
-				ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
-				.innerJoin(this.antennaNotesRepository.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
+			const noteIdsRes = await this.redisClient.xrevrange(
+				`antennaTimeline:${antenna.id}`,
+				ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
+				'-',
+				'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
+
+			if (noteIdsRes.length === 0) {
+				return [];
+			}
+
+			const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
+
+			if (noteIds.length === 0) {
+				return [];
+			}
+
+			const query = this.notesRepository.createQueryBuilder('note')
+				.where('note.id IN (:...noteIds)', { noteIds: noteIds })
 				.innerJoinAndSelect('note.user', 'user')
 				.leftJoinAndSelect('user.avatar', 'avatar')
 				.leftJoinAndSelect('user.banner', 'banner')
@@ -86,16 +104,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				.leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
 				.leftJoinAndSelect('renote.user', 'renoteUser')
 				.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
-				.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
-				.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
+				.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
 
 			this.queryService.generateVisibilityQuery(query, me);
 			this.queryService.generateMutedUserQuery(query, me);
 			this.queryService.generateBlockedUserQuery(query, me);
 
-			const notes = await query
-				.take(ps.limit)
-				.getMany();
+			const notes = await query.getMany();
+			notes.sort((a, b) => a.id > b.id ? -1 : 1);
 
 			if (notes.length > 0) {
 				this.noteReadService.read(me.id, notes);

From 92ddebb3fd46da4e75a91f9afb221088744f222a Mon Sep 17 00:00:00 2001
From: nexryai <61890205+nexryai@users.noreply.github.com>
Date: Mon, 3 Apr 2023 12:47:30 +0900
Subject: [PATCH 10/11] =?UTF-8?q?=E3=82=AA=E3=83=96=E3=82=B8=E3=82=A7?=
 =?UTF-8?q?=E3=82=AF=E3=83=88=E3=82=B9=E3=83=88=E3=83=AC=E3=83=BC=E3=82=B8?=
 =?UTF-8?q?=E3=81=AE=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2=E3=82=92=E5=88=86?=
 =?UTF-8?q?=E3=81=8B=E3=82=8A=E3=82=84=E3=81=99=E3=81=8F=E3=81=99=E3=82=8B?=
 =?UTF-8?q?=20(#10456)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance: Improved object storage configuration page

* Update ja-JP.yml

* Update CHANGELOG
---
 CHANGELOG.md                                         | 1 +
 locales/ja-JP.yml                                    | 1 +
 packages/frontend/src/pages/admin/object-storage.vue | 6 ++++--
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 711d9db62..0b33c767e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
 ### Client
 - 検索ページでURLを入力した際に照会したときと同等の挙動をするように
 - ノートのリアクションを大きく表示するオプションを追加
+- オブジェクトストレージの設定画面を分かりやすく
 
 ### Server
 -
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a9c54810a..a4f1d802c 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフに
 objectStorageUseProxy: "Proxyを利用する"
 objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
 objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
+s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
 serverLogs: "サーバーログ"
 deleteAll: "全て削除"
 showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index cbe38b2d8..704b27c17 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -7,7 +7,7 @@
 				<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
 
 				<template v-if="useObjectStorage">
-					<MkInput v-model="objectStorageBaseUrl">
+					<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'">
 						<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
 						<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
 					</MkInput>
@@ -22,8 +22,9 @@
 						<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
 					</MkInput>
 
-					<MkInput v-model="objectStorageEndpoint">
+					<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
 						<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
+						<template #prefix>https://</template>
 						<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
 					</MkInput>
 
@@ -60,6 +61,7 @@
 
 					<MkSwitch v-model="objectStorageS3ForcePathStyle">
 						<template #label>s3ForcePathStyle</template>
+						<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
 					</MkSwitch>
 				</template>
 			</div>

From 32149f51228aea0c5de42a8242b1c56c509db1f3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 3 Apr 2023 14:11:08 +0900
Subject: [PATCH 11/11] Update CHANGELOG.md

---
 CHANGELOG.md | 21 +++++++++++----------
 1 file changed, 11 insertions(+), 10 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8612220c8..5cee783ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,7 @@
 -
 
 ### Client
-- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
-  - 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
-	  - 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
-		- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
-		  - 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
-			- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
-	- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
+- 
 
 ### Server
 -
@@ -23,16 +17,23 @@
 ### General
 - チャンネルをお気に入りに登録できるように
 - チャンネルにノートをピン留めできるように
-- アンテナのタイムライン取得時のパフォーマンスを向上
-- チャンネルのタイムライン取得時のパフォーマンスを向上
 
 ### Client
 - 検索ページでURLを入力した際に照会したときと同等の挙動をするように
 - ノートのリアクションを大きく表示するオプションを追加
 - オブジェクトストレージの設定画面を分かりやすく
+- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
+  - 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
+	  - 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
+		- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
+		  - 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
+			- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
+	- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
 
 ### Server
--
+- ノート作成時のパフォーマンスを向上
+- アンテナのタイムライン取得時のパフォーマンスを向上
+- チャンネルのタイムライン取得時のパフォーマンスを向上
 
 ## 13.10.3