diff --git a/packages/backend/src/server/api/stream/channels/index.ts b/packages/backend/src/server/api/stream/channels/index.ts index d422edde8..926b0d4e4 100644 --- a/packages/backend/src/server/api/stream/channels/index.ts +++ b/packages/backend/src/server/api/stream/channels/index.ts @@ -2,6 +2,7 @@ import main from './main.js'; import homeTimeline from './home-timeline.js'; import localTimeline from './local-timeline.js'; import hybridTimeline from './hybrid-timeline.js'; +import recommendedTimeline from './recommended-timeline.js'; import globalTimeline from './global-timeline.js'; import serverStats from './server-stats.js'; import queueStats from './queue-stats.js'; @@ -18,6 +19,7 @@ export default { main, homeTimeline, localTimeline, + recommendedTimeline, hybridTimeline, globalTimeline, serverStats, diff --git a/packages/backend/test/streaming.ts b/packages/backend/test/streaming.ts index 621d07f9c..c2043ea56 100644 --- a/packages/backend/test/streaming.ts +++ b/packages/backend/test/streaming.ts @@ -223,6 +223,78 @@ describe('Streaming', () => { }); }); + describe('Recommended Timeline', () => { + it('自分の投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, ayano), // ayano posts + msg => msg.type === 'note' && msg.body.text === 'foo' + ); + + assert.strictEqual(fired, true); + }); + + it('フォローしていないローカルユーザーの投稿が流れる', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, chitose), // chitose posts + msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose + ); + + assert.strictEqual(fired, true); + }); + + it('リモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, chinatsu), // chinatsu posts + msg => msg.type === 'note' && msg.body.userId === chinatsu.id // wait chinatsu + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしてたとしてもリモートユーザーの投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo' }, akari), // akari posts + msg => msg.type === 'note' && msg.body.userId === akari.id // wait akari + ); + + assert.strictEqual(fired, false); + }); + + it('ホーム指定の投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'home' }, kyoko), // kyoko home posts + msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしているローカルユーザーのダイレクト投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'specified', visibleUserIds: [ayano.id] }, kyoko), // kyoko DM => ayano + msg => msg.type === 'note' && msg.body.userId === kyoko.id // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', async () => { + const fired = await waitFire( + ayano, 'recommendedTimeline', // ayano:Local + () => api('notes/create', { text: 'foo', visibility: 'followers' }, chitose), + msg => msg.type === 'note' && msg.body.userId === chitose.id // wait chitose + ); + + assert.strictEqual(fired, false); + }); + }); + describe('Hybrid Timeline', () => { it('自分の投稿が流れる', async () => { const fired = await waitFire( diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue index a3fa27ab7..0b279c153 100644 --- a/packages/client/src/components/timeline.vue +++ b/packages/client/src/components/timeline.vue @@ -77,6 +77,10 @@ if (props.src === 'antenna') { endpoint = 'notes/local-timeline'; connection = stream.useChannel('localTimeline'); connection.on('note', prepend); +} else if (props.src === 'recommended') { + endpoint = 'notes/recommended-timeline'; + connection = stream.useChannel('recommendedTimeline'); + connection.on('note', prepend); } else if (props.src === 'social') { endpoint = 'notes/hybrid-timeline'; connection = stream.useChannel('hybridTimeline');