From 385969e9f56a39a1e5547b94901d155e1e811263 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 12 Jul 2024 16:25:44 +0900
Subject: fix(frontend): フォーカスの挙動を修正 (#14158)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): 直前のパターンを記録するように

* fix(frontend): フォーカス/タブ移動に関する挙動を調整 (#226)

Cherry-pick commit e8c030673326871edf3623cf2b8675d68f9e1b13

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>

* focusのデザイン修正

* move scripts

* Modalにfocus trapを追加

* 記録するホットキーはレートリミット式にする

* escキーのハンドリングをMkModalに統一

* fix

* enterで子メニューを開けるように

* lint

* fix focus trap

* improve switch accessibility

* 一部のmodalのフォーカストラップが外れない問題を修正

* fix

* fix

* Revert "記録するホットキーはレートリミット式にする"

This reverts commit 40a7509286a87911ad4cc06d9482e8a2e5d0e7e8.

* Revert "fix(frontend): 直前のパターンを記録するように"

This reverts commit 5372b2594023952cff34aa62253ed4efef15b5dd.

* Revert "Revert "fix(frontend): 直前のパターンを記録するように""

This reverts commit a9bb52e799e110927ad92cd8f26af980819334e1.

* Revert "Revert "記録するホットキーはレートリミット式にする""

This reverts commit bdac34273e0bc5f13604c7e2f9fa6b1321a0df3d.

* 試験的にCypressでのFocustrapを無効化

* fix

* fix focus-trap

* Update Changelog

* :v:

* fix focustrap invocation logic

* スクロールがsticky headerを考慮するように

* :art:

* スタイルの微調整

* :art:

* remove deprecated key aliases

* focusElementが足りなかったので修正

* preview系にfocus時スタイルが足りなかったので修正

* `returnFocusElement` -> `returnFocusTo`

* lint

* Update packages/frontend/src/components/MkModalWindow.vue

* Apply suggestions from code review

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>

* keydownイベントをまとめる

* use correct pesudo-element selector

* fix

* rename

---------

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
 packages/frontend/src/components/MkTutorialDialog.Timeline.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'packages/frontend/src/components/MkTutorialDialog.Timeline.vue')

diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 57f26e86a7..6f2930ebc9 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -56,7 +56,7 @@ import { i18n } from '@/i18n.js';
 	font-weight: bold;
 	text-align: left;
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		width: calc(100% - 38px);
-- 
cgit v1.2.3-freya


From fccc5b6d620ca6893a0a5649ea9f0d0693727b3b Mon Sep 17 00:00:00 2001
From: anatawa12 <anatawa12@icloud.com>
Date: Tue, 30 Jul 2024 20:13:00 +0900
Subject: frontend timeline fixes & improvements (#13727)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix: withRepliesがオフのときにwithFilesのとぐるをいじれない問題

* fix: type errors in tl-column

* fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されない

* refactor: タイムラインの各種知識を一つのファイルに統合

fix: ウィジェットのタイムライン選択欄に表示できないタイムラインが表示される

* docs(changelog): timeline improvements

* fix: missing license header

* chore: timeline > basic timeline

* use BasicTimelineType in deck-store

* Update CHANGELOG.md

---------

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
---
 CHANGELOG.md                                       |  3 +
 packages/frontend/src/components/MkTimeline.vue    |  3 +-
 .../src/components/MkTutorialDialog.Timeline.vue   |  9 ++-
 packages/frontend/src/pages/timeline.vue           | 73 +++++++---------------
 packages/frontend/src/timelines.ts                 | 56 +++++++++++++++++
 packages/frontend/src/ui/deck/deck-store.ts        |  3 +-
 packages/frontend/src/ui/deck/tl-column.vue        | 30 +++------
 packages/frontend/src/widgets/WidgetTimeline.vue   | 34 +++-------
 8 files changed, 108 insertions(+), 103 deletions(-)
 create mode 100644 packages/frontend/src/timelines.ts

(limited to 'packages/frontend/src/components/MkTutorialDialog.Timeline.vue')

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',
-	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,
+}))), ...availableBasicTimelines().map(tl => ({
+	key: tl,
+	title: i18n.ts._timelines[tl],
+	icon: basicTimelineIconClass(tl),
 	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;
 	});
 };
-- 
cgit v1.2.3-freya

