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