diff --git a/CHANGELOG.md b/CHANGELOG.md index 89d5ac214d..08e75ea200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,9 @@ - Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正 - Fix: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正 - Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正 +- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正 +- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正 +- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正 ### Server - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 03dccb18e9..ca87316bf7 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; import * as Misskey from 'misskey-js'; +import type { BasicTimelineType } from '@/timelines.js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import { useStream } from '@/stream.js'; @@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js'; import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ - src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; + src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role'; list?: string; antenna?: string; channel?: string; diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue index 6f2930ebc9..2d4da3fbd4 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -7,10 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="_gaps"> <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div> <div class="_gaps_s"> - <div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div> - <div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div> - <div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div> - <div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div> + <div v-for="tl in basicTimelineTypes"> + <i :class="basicTimelineIconClass(tl)"></i> <b>{{ i18n.ts._timelines[tl] }}</b> … {{ i18n.ts._initialTutorial._timeline[tl] }} + </div> </div> <div class="_gaps_s"> <div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div> @@ -22,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only <a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a> </template> </I18n> - </div> </template> <script setup lang="ts"> import { i18n } from '@/i18n.js'; +import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js'; </script> <style lang="scss" module> diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 813cc326d0..c5905c7ada 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer :contentMax="800"> <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> <div :key="src" ref="rootEl"> - <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> + <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> {{ i18n.ts._timelineDescription[src] }} </MkInfo> <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> @@ -45,7 +45,6 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; import { $i } from '@/account.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; @@ -53,17 +52,15 @@ import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; import { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; +import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; provide('shouldOmitHeaderTitle', true); -const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); -const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); - const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); const rootEl = shallowRef<HTMLElement>(); const queue = ref(0); -const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global'); +const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global'); const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x), @@ -74,7 +71,11 @@ const withRenotes = computed<boolean>({ }); // computed内での無限ループを防ぐためのフラグ -const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies'); +const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>( + defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' : + defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' : + false, +); const withReplies = computed<boolean>({ get: () => { @@ -229,7 +230,7 @@ function focus(): void { } function closeTutorial(): void { - if (!['home', 'local', 'social', 'global'].includes(src.value)) return; + if (!isBasicTimeline(src.value)) return; const before = defaultStore.state.timelineTutorials; before[src.value] = true; defaultStore.set('timelineTutorials', before); @@ -245,7 +246,7 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, src.value === 'local' || src.value === 'social' ? { + }, isBasicTimeline(src.value) && hasWithReplies(src.value) ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, @@ -258,7 +259,7 @@ const headerActions = computed(() => { type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: src.value === 'local' || src.value === 'social' ? withReplies : false, + disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false, }], ev.currentTarget ?? ev.target); }, }, @@ -280,32 +281,12 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList title: l.name, icon: 'ti ti-star', iconOnly: true, -}))), { - key: 'home', - title: i18n.ts._timelines.home, - icon: 'ti ti-home', +}))), ...availableBasicTimelines().map(tl => ({ + key: tl, + title: i18n.ts._timelines[tl], + icon: basicTimelineIconClass(tl), iconOnly: true, -}, ...(isLocalTimelineAvailable ? [{ - key: 'local', - title: i18n.ts._timelines.local, - icon: 'ti ti-planet', - iconOnly: true, -}, { - 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, @@ -317,24 +298,16 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList 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, - }] : []), -] as Tab[]); +const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({ + key: tl, + title: i18n.ts._timelines[tl], + icon: basicTimelineIconClass(tl), + iconOnly: true, +}))] as Tab[]); definePageMetadata(() => ({ title: i18n.ts.timeline, - icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : 'ti ti-home', + icon: isBasicTimeline(src.value) ? basicTimelineIconClass(src.value) : 'ti ti-home', })); </script> diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts new file mode 100644 index 0000000000..3ef95fd272 --- /dev/null +++ b/packages/frontend/src/timelines.ts @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { $i } from '@/account.js'; +import { instance } from '@/instance.js'; + +export const basicTimelineTypes = [ + 'home', + 'local', + 'social', + 'global', +] as const; + +export type BasicTimelineType = typeof basicTimelineTypes[number]; + +export function isBasicTimeline(timeline: string): timeline is BasicTimelineType { + return basicTimelineTypes.includes(timeline as BasicTimelineType); +} + +export function basicTimelineIconClass(timeline: BasicTimelineType): string { + switch (timeline) { + case 'home': + return 'ti ti-home'; + case 'local': + return 'ti ti-planet'; + case 'social': + return 'ti ti-universe'; + case 'global': + return 'ti ti-whirl'; + } +} + +export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean { + switch (timeline) { + case 'home': + return $i != null; + case 'local': + return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); + case 'social': + return $i != null && instance.policies.ltlAvailable; + case 'global': + return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); + default: + return false; + } +} + +export function availableBasicTimelines(): BasicTimelineType[] { + return basicTimelineTypes.filter(isAvailableBasicTimeline); +} + +export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean { + return timeline === 'local' || timeline === 'social'; +} diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 139621cf57..eb587554b9 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -6,6 +6,7 @@ import { throttle } from 'throttle-debounce'; import { markRaw } from 'vue'; import { notificationTypes } from 'misskey-js'; +import type { BasicTimelineType } from '@/timelines.js'; import { Storage } from '@/pizzax.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { deepClone } from '@/scripts/clone.js'; @@ -45,7 +46,7 @@ export type Column = { channelId?: string; roleId?: string; excludeTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global'; + tl?: BasicTimelineType; withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index b4bc8bb748..e210ee7b7a 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }"> <template #header> - <i v-if="column.tl === 'home'" class="ti ti-home"></i> - <i v-else-if="column.tl === 'local'" class="ti ti-planet"></i> - <i v-else-if="column.tl === 'social'" class="ti ti-universe"></i> - <i v-else-if="column.tl === 'global'" class="ti ti-whirl"></i> + <i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/> <span style="margin-left: 8px;">{{ column.name }}</span> </template> - <div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled"> + <div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled"> <p :class="$style.disabledTitle"> <i class="ti ti-circle-minus"></i> {{ i18n.ts._disabledTimeline.title }} @@ -34,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { onMounted, watch, ref, shallowRef } from 'vue'; +import { onMounted, watch, ref, shallowRef, computed } from 'vue'; import XColumn from './column.vue'; import { removeColumn, updateColumn, Column } from './deck-store.js'; +import type { MenuItem } from '@/types/menu.js'; import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; -import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; +import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import { instance } from '@/instance.js'; -import { MenuItem } from '@/types/menu.js'; import { SoundStore } from '@/store.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import * as sound from '@/scripts/sound.js'; @@ -52,11 +49,8 @@ const props = defineProps<{ isStacked: boolean; }>(); -const disabled = ref(false); const timeline = shallowRef<InstanceType<typeof MkTimeline>>(); -const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); -const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 }); const withRenotes = ref(props.column.withRenotes ?? true); const withReplies = ref(props.column.withReplies ?? false); @@ -87,10 +81,6 @@ watch(soundSetting, v => { onMounted(() => { if (props.column.tl == null) { setType(); - } else if ($i) { - disabled.value = ( - (!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) || - (!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl))); } }); @@ -115,7 +105,7 @@ async function setType() { } if (src == null) return; updateColumn(props.column.id, { - tl: src, + tl: src ?? undefined, }); } @@ -123,7 +113,7 @@ function onNote() { sound.playMisskeySfxFile(soundSetting.value); } -const menu: MenuItem[] = [{ +const menu = computed<MenuItem[]>(() => [{ icon: 'ti ti-pencil', text: i18n.ts.timeline, action: setType, @@ -135,7 +125,7 @@ const menu: MenuItem[] = [{ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, -}, props.column.tl === 'local' || props.column.tl === 'social' ? { +}, hasWithReplies(props.column.tl) ? { type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, ref: withReplies, @@ -144,8 +134,8 @@ const menu: MenuItem[] = [{ type: 'switch', text: i18n.ts.fileAttachedOnly, ref: onlyFiles, - disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false, -}]; + disabled: hasWithReplies(props.column.tl) ? withReplies : false, +}]); </script> <style lang="scss" module> diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue index 150e838582..d02f9b8e22 100644 --- a/packages/frontend/src/widgets/WidgetTimeline.vue +++ b/packages/frontend/src/widgets/WidgetTimeline.vue @@ -6,10 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkContainer :showHeader="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" data-cy-mkw-timeline class="mkw-timeline"> <template #icon> - <i v-if="widgetProps.src === 'home'" class="ti ti-home"></i> - <i v-else-if="widgetProps.src === 'local'" class="ti ti-planet"></i> - <i v-else-if="widgetProps.src === 'social'" class="ti ti-universe"></i> - <i v-else-if="widgetProps.src === 'global'" class="ti ti-whirl"></i> + <i v-if="isBasicTimeline(widgetProps.src)" :class="basicTimelineIconClass(widgetProps.src)"></i> <i v-else-if="widgetProps.src === 'list'" class="ti ti-list"></i> <i v-else-if="widgetProps.src === 'antenna'" class="ti ti-antenna"></i> </template> @@ -20,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </button> </template> - <div v-if="(((widgetProps.src === 'local' || widgetProps.src === 'social') && !isLocalTimelineAvailable) || (widgetProps.src === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled"> + <div v-if="isBasicTimeline(widgetProps.src) && !isAvailableBasicTimeline(widgetProps.src)" :class="$style.disabled"> <p :class="$style.disabledTitle"> <i class="ti ti-minus"></i> {{ i18n.ts._disabledTimeline.title }} @@ -42,12 +39,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; import MkTimeline from '@/components/MkTimeline.vue'; import { i18n } from '@/i18n.js'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; +import { availableBasicTimelines, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; const name = 'timeline'; -const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); -const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); const widgetPropsDef = { showHeader: { @@ -115,23 +109,11 @@ const choose = async (ev) => { setSrc('list'); }, })); - os.popupMenu([{ - text: i18n.ts._timelines.home, - icon: 'ti ti-home', - action: () => { setSrc('home'); }, - }, { - text: i18n.ts._timelines.local, - icon: 'ti ti-planet', - action: () => { setSrc('local'); }, - }, { - text: i18n.ts._timelines.social, - icon: 'ti ti-universe', - action: () => { setSrc('social'); }, - }, { - text: i18n.ts._timelines.global, - icon: 'ti ti-whirl', - action: () => { setSrc('global'); }, - }, antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { + os.popupMenu([...availableBasicTimelines().map(tl => ({ + text: i18n.ts._timelines[tl], + icon: basicTimelineIconClass(tl), + action: () => { setSrc(tl); }, + })), antennaItems.length > 0 ? { type: 'divider' } : undefined, ...antennaItems, listItems.length > 0 ? { type: 'divider' } : undefined, ...listItems], ev.currentTarget ?? ev.target).then(() => { menuOpened.value = false; }); };