From 7bd7fe996c720eb16a2f2b2c2944e9baff536085 Mon Sep 17 00:00:00 2001 From: mattyatea <mattyacocacora0@gmail.com> Date: Fri, 20 Oct 2023 11:21:32 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=E3=81=AA=E3=82=93=E3=81=8BReply?= =?UTF-8?q?=E3=81=8C=E3=81=8A=E3=81=8B=E3=81=97=E3=81=84=E3=81=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/endpoints/notes/global-timeline.ts | 10 - .../api/stream/channels/global-timeline.ts | 22 +- .../api/stream/channels/home-timeline.ts | 9 +- .../api/stream/channels/hybrid-timeline.ts | 8 +- .../api/stream/channels/local-timeline.ts | 20 +- packages/frontend/src/pages/timeline.vue | 323 +++++++++--------- 6 files changed, 170 insertions(+), 222 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 834dcb23aa..be7557c213 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -87,16 +87,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } - - if (ps.withRenotes === false) { - query.andWhere(new Brackets(qb => { - qb.orWhere('note.renoteId IS NULL'); - qb.orWhere(new Brackets(qb => { - qb.orWhere('note.text IS NOT NULL'); - qb.orWhere('note.fileIds != \'{}\''); - })); - })); - } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 38fc49d679..553c44071f 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -18,9 +18,8 @@ class GlobalTimelineChannel extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = false; public static requireCredential = false; - private withReplies: boolean; - private withFiles: boolean; private withRenotes: boolean; + private withFiles: boolean; constructor( private metaService: MetaService, @@ -39,7 +38,6 @@ class GlobalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; this.withFiles = params.withFiles ?? false; @@ -49,25 +47,11 @@ class GlobalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (note.visibility !== 'public') return; if (note.channelId != null) return; - // ファイルを含まない投稿は除外 - if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; - - // リプライなら再pack - if (note.replyId != null) { - note.reply = await this.noteEntityService.pack(note.replyId, this.user, { - detail: true, - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { - detail: true, - }); - } - // 関係ない返信は除外 if (note.reply && !this.following[note.userId]?.withReplies) { const reply = note.reply; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index b855f1835c..46071e82fa 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -16,9 +16,8 @@ class HomeTimelineChannel extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = false; public static requireCredential = true; - private withReplies: boolean; - private withFiles: boolean; private withRenotes: boolean; + private withFiles: boolean; constructor( private noteEntityService: NoteEntityService, @@ -32,9 +31,8 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; - this.withFiles = params.withFiles as boolean; + this.withFiles = params.withFiles ?? false; this.subscriber.on('notesStream', this.onNote); } @@ -55,9 +53,6 @@ class HomeTimelineChannel extends Channel { // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return; - // ファイルを含まない投稿は除外 - if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; - if (note.visibility === 'followers') { if (!isMe && !Object.hasOwn(this.following, note.userId)) return; } else if (note.visibility === 'specified') { diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 788ca9678d..8d7973d907 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -39,9 +39,9 @@ class HybridTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withReplies = params.withReplies ?? false; this.withRenotes = params.withRenotes ?? true; - this.withFiles = params.withFiles as boolean; + this.withReplies = params.withReplies ?? false; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -64,9 +64,6 @@ class HybridTimelineChannel extends Channel { (note.channelId != null && this.followingChannels.has(note.channelId)) )) return; - // ファイルを含まない投稿は除外 - if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; - if (note.visibility === 'followers') { if (!isMe && !Object.hasOwn(this.following, note.userId)) return; } else if (note.visibility === 'specified') { @@ -75,7 +72,6 @@ class HybridTimelineChannel extends Channel { // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances))) return; - // 関係ない返信は除外 if (note.reply && !this.following[note.userId]?.withReplies && !this.withReplies) { const reply = note.reply; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index f93f57d1ed..9dd05b9b08 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -40,7 +40,7 @@ class LocalTimelineChannel extends Channel { this.withRenotes = params.withRenotes ?? true; this.withReplies = params.withReplies ?? false; - this.withFiles = params.withFiles as boolean; + this.withFiles = params.withFiles ?? false; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -48,26 +48,12 @@ class LocalTimelineChannel extends Channel { @bindThis private async onNote(note: Packed<'Note'>) { + if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return; + if (note.user.host !== null) return; if (note.visibility !== 'public') return; if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; - // ファイルを含まない投稿は除外 - if (this.withFiles && (note.files === undefined || note.files.length === 0)) return; - - // リプライなら再pack - if (note.replyId != null) { - note.reply = await this.noteEntityService.pack(note.replyId, this.user, { - detail: true, - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { - detail: true, - }); - } - // 関係ない返信は除外 if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) { const reply = note.reply; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 5b6dc80e59..3d18ab2d3b 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -4,30 +4,30 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> - <MkStickyContainer> - <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> - <MkSpacer :contentMax="800"> - <div ref="rootEl" v-hotkey.global="keymap"> - <XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> - <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> + <MkStickyContainer> + <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> + <MkSpacer :contentMax="800"> + <div ref="rootEl" v-hotkey.global="keymap"> + <XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> + <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> - <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> - <div :class="$style.tl"> - <MkTimeline - ref="tlComponent" - :key="src + withRenotes + withReplies + onlyFiles" - :src="src.split(':')[0]" - :list="src.split(':')[1]" - :withRenotes="withRenotes" - :withReplies="withReplies" - :onlyFiles="onlyFiles" - :sound="true" - @queue="queueUpdated" - /> - </div> - </div> - </MkSpacer> -</MkStickyContainer> + <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> + <div :class="$style.tl"> + <MkTimeline + ref="tlComponent" + :key="src + withRenotes + withReplies + onlyFiles" + :src="src.split(':')[0]" + :list="src.split(':')[1]" + :withRenotes="withRenotes" + :withReplies="withReplies" + :onlyFiles="onlyFiles" + :sound="true" + @queue="queueUpdated" + /> + </div> + </div> + </MkSpacer> + </MkStickyContainer> </template> <script lang="ts" setup> @@ -52,7 +52,7 @@ const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue')); const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable && defaultStore.state.showGlobalTimeline) || ($i != null && $i.policies.gtlAvailable && defaultStore.state.showGlobalTimeline); const keymap = { - 't': focus, + 't': focus, }; const tlComponent = $shallowRef<InstanceType<typeof MkTimeline>>(); @@ -61,205 +61,202 @@ const rootEl = $shallowRef<HTMLElement>(); let queue = $ref(0); let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); -const withReplies_store = computed(defaultStore.makeGetterSetter('withRenotes')) -const withRenotes_store = computed(defaultStore.makeGetterSetter('withReplies')) -const onlyFiles_store = computed(defaultStore.makeGetterSetter('onlyFiles')) -const withRenotes = $ref(defaultStore.state.onlyAndWithSave ? withRenotes_store : true); -const withReplies = $ref(defaultStore.state.onlyAndWithSave ? withReplies_store : true); -const onlyFiles = $ref(defaultStore.state.onlyAndWithSave ? onlyFiles_store : false); +const withRenotes = $ref(true); +const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false); +const onlyFiles = $ref(false); const isShowMediaTimeline = $ref(defaultStore.state.showMediaTimeline) + watch($$(src), () => queue = 0); watch($$(withReplies), (x) => { - if ($i) defaultStore.set('tlWithReplies', x); + if ($i) defaultStore.set('tlWithReplies', x); }); function queueUpdated(q: number): void { - queue = q; + queue = q; } function top(): void { - if (rootEl) scroll(rootEl, { top: 0 }); + if (rootEl) scroll(rootEl, { top: 0 }); } async function chooseList(ev: MouseEvent): Promise<void> { - const lists = await userListsCache.fetch(); - const items = lists.map(list => ({ - type: 'link' as const, - text: list.name, - to: `/timeline/list/${list.id}`, - })); - os.popupMenu(items, ev.currentTarget ?? ev.target); + const lists = await userListsCache.fetch(); + const items = lists.map(list => ({ + type: 'link' as const, + text: list.name, + to: `/timeline/list/${list.id}`, + })); + os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseAntenna(ev: MouseEvent): Promise<void> { - const antennas = await antennasCache.fetch(); - const items = antennas.map(antenna => ({ - type: 'link' as const, - text: antenna.name, - indicate: antenna.hasUnreadNote, - to: `/timeline/antenna/${antenna.id}`, - })); - os.popupMenu(items, ev.currentTarget ?? ev.target); + const antennas = await antennasCache.fetch(); + const items = antennas.map(antenna => ({ + type: 'link' as const, + text: antenna.name, + indicate: antenna.hasUnreadNote, + to: `/timeline/antenna/${antenna.id}`, + })); + os.popupMenu(items, ev.currentTarget ?? ev.target); } async function chooseChannel(ev: MouseEvent): Promise<void> { - const channels = await os.api('channels/my-favorites', { - limit: 100, - }); - const items = channels.map(channel => ({ - type: 'link' as const, - text: channel.name, - indicate: channel.hasUnreadNote, - to: `/channels/${channel.id}`, - })); - os.popupMenu(items, ev.currentTarget ?? ev.target); + const channels = await os.api('channels/my-favorites', { + limit: 100, + }); + const items = channels.map(channel => ({ + type: 'link' as const, + text: channel.name, + indicate: channel.hasUnreadNote, + to: `/channels/${channel.id}`, + })); + os.popupMenu(items, ev.currentTarget ?? ev.target); } -function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void { - let userList = null; - if (newSrc.startsWith('userList:')) { - const id = newSrc.substring('userList:'.length); - userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id); - } - defaultStore.set('tl', { - src: newSrc, - userList, - }); - srcWhenNotSignin = newSrc; +function saveSrc(newSrc: 'home' | 'local' | 'media' | 'social' | 'global' | `list:${string}`): void { + let userList = null; + if (newSrc.startsWith('userList:')) { + const id = newSrc.substring('userList:'.length); + userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id); + } + defaultStore.set('tl', { + src: newSrc, + userList, + }); + srcWhenNotSignin = newSrc; } async function timetravel(): Promise<void> { - const { canceled, result: date } = await os.inputDate({ - title: i18n.ts.date, - }); - if (canceled) return; + const { canceled, result: date } = await os.inputDate({ + title: i18n.ts.date, + }); + if (canceled) return; - tlComponent.timetravel(date); + tlComponent.timetravel(date); } function focus(): void { - tlComponent.focus(); + tlComponent.focus(); } const headerActions = $computed(() => [{ - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: (ev) => { - os.popupMenu([{ - type: 'switch', - text: i18n.ts.showRenotes, - icon: 'ti ti-repeat', - ref: $$(withRenotes), - }, { - type: 'switch', - text: i18n.ts.withReplies, - icon: 'ti ti-arrow-back-up', - ref: $$(withReplies), - }, { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - icon: 'ti ti-photo', - ref: $$(onlyFiles), - }], ev.currentTarget ?? ev.target); - }, + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: (ev) => { + os.popupMenu([{ + type: 'switch', + text: i18n.ts.showRenotes, + icon: 'ti ti-repeat', + ref: $$(withRenotes), + }, src === 'local' || src === 'social' ? { + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: $$(withReplies), + } : undefined, { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + icon: 'ti ti-photo', + ref: $$(onlyFiles), + }], ev.currentTarget ?? ev.target); + }, }]); const headerTabs = $computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({ - key: 'list:' + l.id, - title: l.name, - icon: 'ti ti-star', - iconOnly: true, + key: 'list:' + l.id, + title: l.name, + icon: 'ti ti-star', + iconOnly: true, }))), { - key: 'home', - title: i18n.ts._timelines.home, - icon: 'ti ti-home', - iconOnly: true, + key: 'home', + title: i18n.ts._timelines.home, + icon: 'ti ti-home', + iconOnly: true, }, ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ti ti-planet', - iconOnly: true, + key: 'local', + title: i18n.ts._timelines.local, + icon: 'ti ti-planet', + iconOnly: true, }, ...(isShowMediaTimeline ? [{ - key: 'media', - title: i18n.ts._timelines.media, - icon: 'ti ti-photo', - iconOnly: true, -}] : []),{ - key: 'social', - title: i18n.ts._timelines.social, - icon: 'ti ti-rocket', - iconOnly: true, -}] : []), ...(isGlobalTimelineAvailable ? [{ - key: 'global', - title: i18n.ts._timelines.global, - icon: 'ti ti-whirl', - iconOnly: true, + key: 'media', + title: i18n.ts._timelines.media, + icon: 'ti ti-photo', + iconOnly: true, }] : []), { - icon: 'ti ti-list', - title: i18n.ts.lists, - iconOnly: true, - onClick: chooseList, + key: 'social', + title: i18n.ts._timelines.social, + icon: 'ti ti-universe', + iconOnly: true, +}] : []), ...(isGlobalTimelineAvailable ? [{ + key: 'global', + title: i18n.ts._timelines.global, + icon: 'ti ti-whirl', + iconOnly: true, +}] : []), { + icon: 'ti ti-list', + title: i18n.ts.lists, + iconOnly: true, + onClick: chooseList, }, { - icon: 'ti ti-antenna', - title: i18n.ts.antennas, - iconOnly: true, - onClick: chooseAntenna, + icon: 'ti ti-antenna', + title: i18n.ts.antennas, + iconOnly: true, + onClick: chooseAntenna, }, { - icon: 'ti ti-device-tv', - title: i18n.ts.channel, - iconOnly: true, - onClick: chooseChannel, + icon: 'ti ti-device-tv', + title: i18n.ts.channel, + iconOnly: true, + onClick: chooseChannel, }] as Tab[]); const headerTabsWhenNotLogin = $computed(() => [ - ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ti ti-planet', - iconOnly: true, - }] : []), - ...(isGlobalTimelineAvailable ? [{ - key: 'global', - title: i18n.ts._timelines.global, - icon: 'ti ti-whirl', - iconOnly: true, - }] : []), + ...(isLocalTimelineAvailable ? [{ + key: 'local', + title: i18n.ts._timelines.local, + icon: 'ti ti-planet', + iconOnly: true, + }] : []), + ...(isGlobalTimelineAvailable ? [{ + key: 'global', + title: i18n.ts._timelines.global, + icon: 'ti ti-whirl', + iconOnly: true, + }] : []), ] as Tab[]); definePageMetadata(computed(() => ({ - title: i18n.ts.timeline, - icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-universe' : src === 'global' ? 'ti ti-whirl' : 'ti ti-home', + title: i18n.ts.timeline, + icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-universe' : src === 'global' ? 'ti ti-whirl' : 'ti ti-home', }))); </script> <style lang="scss" module> .new { - position: sticky; - top: calc(var(--stickyTop, 0px) + 16px); - z-index: 1000; - width: 100%; - margin: calc(-0.675em - 8px) 0; + position: sticky; + top: calc(var(--stickyTop, 0px) + 16px); + z-index: 1000; + width: 100%; + margin: calc(-0.675em - 8px) 0; - &:first-child { - margin-top: calc(-0.675em - 8px - var(--margin)); - } + &:first-child { + margin-top: calc(-0.675em - 8px - var(--margin)); + } } .newButton { - display: block; - margin: var(--margin) auto 0 auto; - padding: 8px 16px; - border-radius: 32px; + display: block; + margin: var(--margin) auto 0 auto; + padding: 8px 16px; + border-radius: 32px; } .postForm { - border-radius: var(--radius); + border-radius: var(--radius); } .tl { - background: var(--bg); - border-radius: var(--radius); - overflow: clip; + background: var(--bg); + border-radius: var(--radius); + overflow: clip; } </style>