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: 投稿フォームにノートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
 | 
			
		||||
- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正
 | 
			
		||||
- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正
 | 
			
		||||
- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
 | 
			
		||||
- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({
 | 
			
		||||
	key: tl,
 | 
			
		||||
	title: i18n.ts._timelines[tl],
 | 
			
		||||
	icon: basicTimelineIconClass(tl),
 | 
			
		||||
	iconOnly: true,
 | 
			
		||||
	}] : []),
 | 
			
		||||
	...(isGlobalTimelineAvailable ? [{
 | 
			
		||||
		key: 'global',
 | 
			
		||||
		title: i18n.ts._timelines.global,
 | 
			
		||||
		icon: 'ti ti-whirl',
 | 
			
		||||
		iconOnly: true,
 | 
			
		||||
	}] : []),
 | 
			
		||||
] as Tab[]);
 | 
			
		||||
}))] 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>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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 { 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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue