From 87c8f9ff953499340496e9c5db09c93eaff08851 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Fri, 19 Mar 2021 20:43:24 +0900
Subject: perf: Reduce database query

---
 src/client/components/note.vue | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

(limited to 'src/client/components/note.vue')

diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 65e09b7802..3b59afd71d 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -325,7 +325,15 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+				this.connection.send('sn', { id: this.appearNote.id });
+				if (this.appearNote.userId !== this.$i.id) {
+					if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) {
+						this.connection.send('readMention', { id: this.appearNote.id });
+					}
+					if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) {
+						this.connection.send('readSpecifiedNote', { id: this.appearNote.id });
+					}
+				}
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
-- 
cgit v1.2.3-freya


From 630464f38d2524ddc5d11d2abd4fddcccc4240d4 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 21 Mar 2021 15:35:02 +0900
Subject: Revert "perf: Reduce database query"

This reverts commit 87c8f9ff953499340496e9c5db09c93eaff08851.
---
 src/client/components/note-detailed.vue    |  10 +--
 src/client/components/note.vue             |  10 +--
 src/client/ui/chat/note.vue                |  10 +--
 src/server/api/endpoints/notes/mentions.ts |   6 +-
 src/server/api/stream/index.ts             |  38 +++--------
 src/services/note/read-mention.ts          |  29 --------
 src/services/note/read-specified-note.ts   |  29 --------
 src/services/note/read.ts                  | 105 +++++++++++++++++++++++++++++
 8 files changed, 123 insertions(+), 114 deletions(-)
 delete mode 100644 src/services/note/read-mention.ts
 delete mode 100644 src/services/note/read-specified-note.ts
 create mode 100644 src/services/note/read.ts

(limited to 'src/client/components/note.vue')

diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 4ad3d2d898..1ef3f43389 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -350,15 +350,7 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send('sn', { id: this.appearNote.id });
-				if (this.appearNote.userId !== this.$i.id) {
-					if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) {
-						this.connection.send('readMention', { id: this.appearNote.id });
-					}
-					if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) {
-						this.connection.send('readSpecifiedNote', { id: this.appearNote.id });
-					}
-				}
+				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 3b59afd71d..65e09b7802 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -325,15 +325,7 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send('sn', { id: this.appearNote.id });
-				if (this.appearNote.userId !== this.$i.id) {
-					if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) {
-						this.connection.send('readMention', { id: this.appearNote.id });
-					}
-					if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) {
-						this.connection.send('readSpecifiedNote', { id: this.appearNote.id });
-					}
-				}
+				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index 29bc61d9c5..5a4a13d889 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -325,15 +325,7 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send('sn', { id: this.appearNote.id });
-				if (this.appearNote.userId !== this.$i.id) {
-					if (this.appearNote.mentions && this.appearNote.mentions.includes(this.$i.id)) {
-						this.connection.send('readMention', { id: this.appearNote.id });
-					}
-					if (this.appearNote.visibleUserIds && this.appearNote.visibleUserIds.includes(this.$i.id)) {
-						this.connection.send('readSpecifiedNote', { id: this.appearNote.id });
-					}
-				}
+				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 56640ec1ab..30844774e0 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -1,12 +1,12 @@
 import $ from 'cafy';
 import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
+import read from '../../../../services/note/read';
 import { Notes, Followings } from '../../../../models';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
 import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Brackets } from 'typeorm';
-import { readMention } from '../../../../services/note/read-mention';
 
 export const meta = {
 	desc: {
@@ -83,7 +83,9 @@ export default define(meta, async (ps, user) => {
 
 	const mentions = await query.take(ps.limit!).getMany();
 
-	readMention(user.id, mentions.map(n => n.id));
+	for (const note of mentions) {
+		read(user.id, note.id);
+	}
 
 	return await Notes.packMany(mentions, user);
 });
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index 748e894f83..f67faee1ce 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -2,6 +2,7 @@ import autobind from 'autobind-decorator';
 import * as websocket from 'websocket';
 import { readNotification } from '../common/read-notification';
 import call from '../call';
+import readNote from '../../../services/note/read';
 import Channel from './channel';
 import channels from './channels';
 import { EventEmitter } from 'events';
@@ -13,8 +14,6 @@ import { AccessToken } from '../../../models/entities/access-token';
 import { UserProfile } from '../../../models/entities/user-profile';
 import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '../../../services/stream';
 import { UserGroup } from '../../../models/entities/user-group';
-import { readMention } from '../../../services/note/read-mention';
-import { readSpecifiedNote } from '../../../services/note/read-specified-note';
 
 /**
  * Main stream connection
@@ -116,10 +115,9 @@ export default class Connection {
 		switch (type) {
 			case 'api': this.onApiRequest(body); break;
 			case 'readNotification': this.onReadNotification(body); break;
-			case 'readMention': this.onReadMention(body); break;
-			case 'readSpecifiedNote': this.onReadSpecifiedNote(body); break;
-			case 'subNote': this.onSubscribeNote(body); break;
-			case 'sn': this.onSubscribeNote(body); break; // alias
+			case 'subNote': this.onSubscribeNote(body, true); break;
+			case 'sn': this.onSubscribeNote(body, true); break; // alias
+			case 's': this.onSubscribeNote(body, false); break;
 			case 'unsubNote': this.onUnsubscribeNote(body); break;
 			case 'un': this.onUnsubscribeNote(body); break; // alias
 			case 'connect': this.onChannelConnectRequested(body); break;
@@ -172,31 +170,11 @@ export default class Connection {
 		readNotification(this.user!.id, [payload.id]);
 	}
 
-	@autobind
-	private onReadMention(payload: any) {
-		if (!payload.id) return;
-		if (this.user) {
-			// TODO: ある程度まとめてreadMentionするようにする
-			// 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadMentionに渡すような実装にする
-			readMention(this.user.id, [payload.id]);
-		}
-	}
-
-	@autobind
-	private onReadSpecifiedNote(payload: any) {
-		if (!payload.id) return;
-		if (this.user) {
-			// TODO: ある程度まとめてreadSpecifiedNoteするようにする
-			// 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadSpecifiedNoteに渡すような実装にする
-			readSpecifiedNote(this.user.id, [payload.id]);
-		}
-	}
-
 	/**
 	 * 投稿購読要求時
 	 */
 	@autobind
-	private onSubscribeNote(payload: any) {
+	private onSubscribeNote(payload: any, read: boolean) {
 		if (!payload.id) return;
 
 		if (this.subscribingNotes[payload.id] == null) {
@@ -208,6 +186,12 @@ export default class Connection {
 		if (this.subscribingNotes[payload.id] === 1) {
 			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
+
+		if (this.user && read) {
+			// TODO: クライアントでタイムライン読み込みなどすると、一度に大量のreadNoteが発生しクエリ数がすごいことになるので、ある程度まとめてreadNoteするようにする
+			// 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadNoteに渡すような実装にする
+			readNote(this.user.id, payload.id);
+		}
 	}
 
 	/**
diff --git a/src/services/note/read-mention.ts b/src/services/note/read-mention.ts
deleted file mode 100644
index 2a668ecd6c..0000000000
--- a/src/services/note/read-mention.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { publishMainStream } from '../stream';
-import { Note } from '../../models/entities/note';
-import { User } from '../../models/entities/user';
-import { NoteUnreads } from '../../models';
-import { In } from 'typeorm';
-
-/**
- * Mark a mention note as read
- */
-export async function readMention(
-	userId: User['id'],
-	noteIds: Note['id'][]
-) {
-	// Remove the records
-	await NoteUnreads.delete({
-		userId: userId,
-		noteId: In(noteIds),
-	});
-
-	const mentionsCount = await NoteUnreads.count({
-		userId: userId,
-		isMentioned: true
-	});
-
-	if (mentionsCount === 0) {
-		// 全て既読になったイベントを発行
-		publishMainStream(userId, 'readAllUnreadMentions');
-	}
-}
diff --git a/src/services/note/read-specified-note.ts b/src/services/note/read-specified-note.ts
deleted file mode 100644
index 0fcb66bf98..0000000000
--- a/src/services/note/read-specified-note.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { publishMainStream } from '../stream';
-import { Note } from '../../models/entities/note';
-import { User } from '../../models/entities/user';
-import { NoteUnreads } from '../../models';
-import { In } from 'typeorm';
-
-/**
- * Mark a specified note as read
- */
-export async function readSpecifiedNote(
-	userId: User['id'],
-	noteIds: Note['id'][]
-) {
-	// Remove the records
-	await NoteUnreads.delete({
-		userId: userId,
-		noteId: In(noteIds),
-	});
-
-	const specifiedCount = await NoteUnreads.count({
-		userId: userId,
-		isSpecified: true
-	});
-
-	if (specifiedCount === 0) {
-		// 全て既読になったイベントを発行
-		publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
-	}
-}
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
new file mode 100644
index 0000000000..5a39ab30b7
--- /dev/null
+++ b/src/services/note/read.ts
@@ -0,0 +1,105 @@
+import { publishMainStream } from '../stream';
+import { Note } from '../../models/entities/note';
+import { User } from '../../models/entities/user';
+import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models';
+import { Not, IsNull } from 'typeorm';
+
+/**
+ * Mark a note as read
+ */
+export default async function(
+	userId: User['id'],
+	noteId: Note['id']
+) {
+	async function careNoteUnreads() {
+		const exist = await NoteUnreads.findOne({
+			userId: userId,
+			noteId: noteId,
+		});
+
+		if (!exist) return;
+
+		// Remove the record
+		await NoteUnreads.delete({
+			userId: userId,
+			noteId: noteId,
+		});
+
+		if (exist.isMentioned) {
+			NoteUnreads.count({
+				userId: userId,
+				isMentioned: true
+			}).then(mentionsCount => {
+				if (mentionsCount === 0) {
+					// 全て既読になったイベントを発行
+					publishMainStream(userId, 'readAllUnreadMentions');
+				}
+			});
+		}
+
+		if (exist.isSpecified) {
+			NoteUnreads.count({
+				userId: userId,
+				isSpecified: true
+			}).then(specifiedCount => {
+				if (specifiedCount === 0) {
+					// 全て既読になったイベントを発行
+					publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
+				}
+			});
+		}
+
+		if (exist.noteChannelId) {
+			NoteUnreads.count({
+				userId: userId,
+				noteChannelId: Not(IsNull())
+			}).then(channelNoteCount => {
+				if (channelNoteCount === 0) {
+					// 全て既読になったイベントを発行
+					publishMainStream(userId, 'readAllChannels');
+				}
+			});
+		}
+	}
+
+	async function careAntenna() {
+		const beforeUnread = await Users.getHasUnreadAntenna(userId);
+		if (!beforeUnread) return;
+
+		const antennas = await Antennas.find({ userId });
+
+		await Promise.all(antennas.map(async antenna => {
+			const countBefore = await AntennaNotes.count({
+				antennaId: antenna.id,
+				read: false
+			});
+
+			if (countBefore === 0) return;
+
+			await AntennaNotes.update({
+				antennaId: antenna.id,
+				noteId: noteId
+			}, {
+				read: true
+			});
+
+			const countAfter = await AntennaNotes.count({
+				antennaId: antenna.id,
+				read: false
+			});
+
+			if (countAfter === 0) {
+				publishMainStream(userId, 'readAntenna', antenna);
+			}
+		}));
+
+		Users.getHasUnreadAntenna(userId).then(unread => {
+			if (!unread) {
+				publishMainStream(userId, 'readAllAntennas');
+			}
+		});
+	}
+
+	careNoteUnreads();
+	careAntenna();
+}
-- 
cgit v1.2.3-freya


From 667d58bad4544d6e9dc75cfc4e6216179e2bc1aa Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 21 Mar 2021 17:38:09 +0900
Subject: better note read handling

---
 src/client/components/note-detailed.vue           |  3 +-
 src/client/components/note.vue                    |  3 +-
 src/client/ui/chat/note.vue                       |  3 +-
 src/daemons/janitor.ts                            |  2 +
 src/server/api/endpoints/notes/mentions.ts        |  4 +-
 src/server/api/stream/channels/antenna.ts         |  2 +
 src/server/api/stream/channels/channel.ts         |  2 +
 src/server/api/stream/channels/global-timeline.ts |  2 +
 src/server/api/stream/channels/hashtag.ts         |  2 +
 src/server/api/stream/channels/home-timeline.ts   |  2 +
 src/server/api/stream/channels/hybrid-timeline.ts |  2 +
 src/server/api/stream/channels/local-timeline.ts  |  2 +
 src/server/api/stream/channels/main.ts            |  8 ++-
 src/server/api/stream/index.ts                    | 58 +++++++++++++---
 src/services/note/read.ts                         | 80 +++++++++--------------
 15 files changed, 109 insertions(+), 66 deletions(-)

(limited to 'src/client/components/note.vue')

diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 1ef3f43389..ea26d31100 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -350,7 +350,8 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 65e09b7802..70f49fef7e 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -325,7 +325,8 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index 5a4a13d889..97275875ca 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -325,7 +325,8 @@ export default defineComponent({
 
 		capture(withHandler = false) {
 			if (this.$i) {
-				this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+				// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+				this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
 				if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
 			}
 		},
diff --git a/src/daemons/janitor.ts b/src/daemons/janitor.ts
index 462ebf915c..c079086427 100644
--- a/src/daemons/janitor.ts
+++ b/src/daemons/janitor.ts
@@ -1,3 +1,5 @@
+// TODO: 消したい
+
 const interval = 30 * 60 * 1000;
 import { AttestationChallenges } from '../models';
 import { LessThan } from 'typeorm';
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 30844774e0..30368ea578 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -83,9 +83,7 @@ export default define(meta, async (ps, user) => {
 
 	const mentions = await query.take(ps.limit!).getMany();
 
-	for (const note of mentions) {
-		read(user.id, note.id);
-	}
+	read(user.id, mentions.map(note => note.id));
 
 	return await Notes.packMany(mentions, user);
 });
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
index b5a792f814..36a474f2ac 100644
--- a/src/server/api/stream/channels/antenna.ts
+++ b/src/server/api/stream/channels/antenna.ts
@@ -27,6 +27,8 @@ export default class extends Channel {
 			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 			if (isMutedUserRelated(note, this.muting)) return;
 
+			this.connection.cacheNote(note);
+
 			this.send('note', note);
 		} else {
 			this.send(type, body);
diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts
index aa570d1ef4..47a52465b2 100644
--- a/src/server/api/stream/channels/channel.ts
+++ b/src/server/api/stream/channels/channel.ts
@@ -43,6 +43,8 @@ export default class extends Channel {
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (isMutedUserRelated(note, this.muting)) return;
 
+		this.connection.cacheNote(note);
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index 8c97e67226..8353f45323 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -56,6 +56,8 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
 		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
 
+		this.connection.cacheNote(note);
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index 41447039d5..1b7f8efcc1 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -37,6 +37,8 @@ export default class extends Channel {
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (isMutedUserRelated(note, this.muting)) return;
 
+		this.connection.cacheNote(note);
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 6cfa6eae7b..59ba31c316 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -64,6 +64,8 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
 		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
 
+		this.connection.cacheNote(note);
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index a9e577cacb..9715e9973f 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -73,6 +73,8 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
 		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
 
+		this.connection.cacheNote(note);
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index a3a5e491fc..e159c72d60 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -58,6 +58,8 @@ export default class extends Channel {
 		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
 		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
 
+		this.connection.cacheNote(note);
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index b69c2ec355..780bc0b89f 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -18,18 +18,22 @@ export default class extends Channel {
 				case 'notification': {
 					if (this.muting.has(body.userId)) return;
 					if (body.note && body.note.isHidden) {
-						body.note = await Notes.pack(body.note.id, this.user, {
+						const note = await Notes.pack(body.note.id, this.user, {
 							detail: true
 						});
+						this.connection.cacheNote(note);
+						body.note = note;
 					}
 					break;
 				}
 				case 'mention': {
 					if (this.muting.has(body.userId)) return;
 					if (body.isHidden) {
-						body = await Notes.pack(body.id, this.user, {
+						const note = await Notes.pack(body.id, this.user, {
 							detail: true
 						});
+						this.connection.cacheNote(note);
+						body = note;
 					}
 					break;
 				}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index f67faee1ce..99ae558696 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -14,6 +14,7 @@ import { AccessToken } from '../../../models/entities/access-token';
 import { UserProfile } from '../../../models/entities/user-profile';
 import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '../../../services/stream';
 import { UserGroup } from '../../../models/entities/user-group';
+import { PackedNote } from '../../../models/repositories/note';
 
 /**
  * Main stream connection
@@ -29,6 +30,7 @@ export default class Connection {
 	public subscriber: EventEmitter;
 	private channels: Channel[] = [];
 	private subscribingNotes: any = {};
+	private cachedNotes: PackedNote[] = [];
 
 	constructor(
 		wsConnection: websocket.connection,
@@ -115,9 +117,9 @@ export default class Connection {
 		switch (type) {
 			case 'api': this.onApiRequest(body); break;
 			case 'readNotification': this.onReadNotification(body); break;
-			case 'subNote': this.onSubscribeNote(body, true); break;
-			case 'sn': this.onSubscribeNote(body, true); break; // alias
-			case 's': this.onSubscribeNote(body, false); break;
+			case 'subNote': this.onSubscribeNote(body); break;
+			case 's': this.onSubscribeNote(body); break; // alias
+			case 'sr': this.onSubscribeNote(body); this.readNote(body); break;
 			case 'unsubNote': this.onUnsubscribeNote(body); break;
 			case 'un': this.onUnsubscribeNote(body); break; // alias
 			case 'connect': this.onChannelConnectRequested(body); break;
@@ -138,6 +140,48 @@ export default class Connection {
 		this.sendMessageToWs(type, body);
 	}
 
+	@autobind
+	public cacheNote(note: PackedNote) {
+		const add = (note: PackedNote) => {
+			const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
+			if (existIndex > -1) {
+				this.cachedNotes[existIndex] = note;
+				return;
+			}
+
+			this.cachedNotes.unshift(note);
+			if (this.cachedNotes.length > 32) {
+				this.cachedNotes.splice(32);
+			}
+		};
+
+		add(note);
+		if (note.reply) add(note.reply);
+		if (note.renote) add(note.renote);
+	}
+
+	@autobind
+	private readNote(body: any) {
+		const id = body.id;
+
+		const note = this.cachedNotes.find(n => n.id === id);
+		if (note == null) return;
+
+		if (this.user && (note.userId !== this.user.id)) {
+			if (note.mentions && note.mentions.includes(this.user.id)) {
+				readNote(this.user.id, [note]);
+			} else if (note.visibleUserIds && note.visibleUserIds.includes(this.user.id)) {
+				readNote(this.user.id, [note]);
+			}
+
+			if (this.followingChannels.has(note.channelId)) {
+				// TODO
+			}
+
+			// TODO: アンテナの既読処理
+		}
+	}
+
 	/**
 	 * APIリクエスト要求時
 	 */
@@ -174,7 +218,7 @@ export default class Connection {
 	 * 投稿購読要求時
 	 */
 	@autobind
-	private onSubscribeNote(payload: any, read: boolean) {
+	private onSubscribeNote(payload: any) {
 		if (!payload.id) return;
 
 		if (this.subscribingNotes[payload.id] == null) {
@@ -186,12 +230,6 @@ export default class Connection {
 		if (this.subscribingNotes[payload.id] === 1) {
 			this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
 		}
-
-		if (this.user && read) {
-			// TODO: クライアントでタイムライン読み込みなどすると、一度に大量のreadNoteが発生しクエリ数がすごいことになるので、ある程度まとめてreadNoteするようにする
-			// 具体的には、この箇所ではキュー的な配列にread予定ノートを溜めておくに留めて、別の箇所で定期的にキューにあるノートを配列でreadNoteに渡すような実装にする
-			readNote(this.user.id, payload.id);
-		}
 	}
 
 	/**
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 5a39ab30b7..35279db411 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -2,70 +2,54 @@ import { publishMainStream } from '../stream';
 import { Note } from '../../models/entities/note';
 import { User } from '../../models/entities/user';
 import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models';
-import { Not, IsNull } from 'typeorm';
+import { Not, IsNull, In } from 'typeorm';
 
 /**
- * Mark a note as read
+ * Mark notes as read
  */
 export default async function(
 	userId: User['id'],
-	noteId: Note['id']
+	noteIds: Note['id'][]
 ) {
 	async function careNoteUnreads() {
-		const exist = await NoteUnreads.findOne({
-			userId: userId,
-			noteId: noteId,
-		});
-
-		if (!exist) return;
-
 		// Remove the record
 		await NoteUnreads.delete({
 			userId: userId,
-			noteId: noteId,
+			noteId: In(noteIds),
 		});
 
-		if (exist.isMentioned) {
-			NoteUnreads.count({
-				userId: userId,
-				isMentioned: true
-			}).then(mentionsCount => {
-				if (mentionsCount === 0) {
-					// 全て既読になったイベントを発行
-					publishMainStream(userId, 'readAllUnreadMentions');
-				}
-			});
-		}
+		NoteUnreads.count({
+			userId: userId,
+			isMentioned: true
+		}).then(mentionsCount => {
+			if (mentionsCount === 0) {
+				// 全て既読になったイベントを発行
+				publishMainStream(userId, 'readAllUnreadMentions');
+			}
+		});
 
-		if (exist.isSpecified) {
-			NoteUnreads.count({
-				userId: userId,
-				isSpecified: true
-			}).then(specifiedCount => {
-				if (specifiedCount === 0) {
-					// 全て既読になったイベントを発行
-					publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
-				}
-			});
-		}
+		NoteUnreads.count({
+			userId: userId,
+			isSpecified: true
+		}).then(specifiedCount => {
+			if (specifiedCount === 0) {
+				// 全て既読になったイベントを発行
+				publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
+			}
+		});
 
-		if (exist.noteChannelId) {
-			NoteUnreads.count({
-				userId: userId,
-				noteChannelId: Not(IsNull())
-			}).then(channelNoteCount => {
-				if (channelNoteCount === 0) {
-					// 全て既読になったイベントを発行
-					publishMainStream(userId, 'readAllChannels');
-				}
-			});
-		}
+		NoteUnreads.count({
+			userId: userId,
+			noteChannelId: Not(IsNull())
+		}).then(channelNoteCount => {
+			if (channelNoteCount === 0) {
+				// 全て既読になったイベントを発行
+				publishMainStream(userId, 'readAllChannels');
+			}
+		});
 	}
 
 	async function careAntenna() {
-		const beforeUnread = await Users.getHasUnreadAntenna(userId);
-		if (!beforeUnread) return;
-
 		const antennas = await Antennas.find({ userId });
 
 		await Promise.all(antennas.map(async antenna => {
@@ -78,7 +62,7 @@ export default async function(
 
 			await AntennaNotes.update({
 				antennaId: antenna.id,
-				noteId: noteId
+				noteId: In(noteIds)
 			}, {
 				read: true
 			});
-- 
cgit v1.2.3-freya

