fix(frontend): vue v3.4.16でタイムラインが正常に表示できない問題を修正
This commit is contained in:
		@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			|||||||
	<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
 | 
						<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
 | 
				
			||||||
	<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 + withRenotes + withReplies + onlyFiles" ref="rootEl" v-hotkey.global="keymap">
 | 
								<div :key="src" ref="rootEl" v-hotkey.global="keymap">
 | 
				
			||||||
				<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="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
 | 
				
			||||||
					{{ i18n.ts._timelineDescription[src] }}
 | 
										{{ i18n.ts._timelineDescription[src] }}
 | 
				
			||||||
				</MkInfo>
 | 
									</MkInfo>
 | 
				
			||||||
@@ -50,6 +50,7 @@ import { $i } from '@/account.js';
 | 
				
			|||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
 | 
					import { definePageMetadata } from '@/scripts/page-metadata.js';
 | 
				
			||||||
import { antennasCache, userListsCache } from '@/cache.js';
 | 
					import { antennasCache, userListsCache } from '@/cache.js';
 | 
				
			||||||
import { deviceKind } from '@/scripts/device-kind.js';
 | 
					import { deviceKind } from '@/scripts/device-kind.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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,10 +75,14 @@ const withRenotes = computed({
 | 
				
			|||||||
	get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
 | 
						get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
 | 
				
			||||||
	set: (x: boolean) => saveTlFilter('withRenotes', x),
 | 
						set: (x: boolean) => saveTlFilter('withRenotes', x),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
const withReplies = computed({
 | 
					
 | 
				
			||||||
 | 
					// computed内での無限ループを防ぐためのフラグ
 | 
				
			||||||
 | 
					const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const withReplies = computed<boolean>({
 | 
				
			||||||
	get: () => {
 | 
						get: () => {
 | 
				
			||||||
		if (!$i) return false;
 | 
							if (!$i) return false;
 | 
				
			||||||
		if (['local', 'social'].includes(src.value) && onlyFiles.value) {
 | 
							if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return defaultStore.reactiveState.tl.value.filter.withReplies;
 | 
								return defaultStore.reactiveState.tl.value.filter.withReplies;
 | 
				
			||||||
@@ -85,9 +90,9 @@ const withReplies = computed({
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	set: (x: boolean) => saveTlFilter('withReplies', x),
 | 
						set: (x: boolean) => saveTlFilter('withReplies', x),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
const onlyFiles = computed({
 | 
					const onlyFiles = computed<boolean>({
 | 
				
			||||||
	get: () => {
 | 
						get: () => {
 | 
				
			||||||
		if (['local', 'social'].includes(src.value) && withReplies.value) {
 | 
							if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
 | 
				
			||||||
			return false;
 | 
								return false;
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return defaultStore.reactiveState.tl.value.filter.onlyFiles;
 | 
								return defaultStore.reactiveState.tl.value.filter.onlyFiles;
 | 
				
			||||||
@@ -95,20 +100,31 @@ const onlyFiles = computed({
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	set: (x: boolean) => saveTlFilter('onlyFiles', x),
 | 
						set: (x: boolean) => saveTlFilter('onlyFiles', x),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
 | 
				
			||||||
 | 
						if (withRepliesTo) {
 | 
				
			||||||
 | 
							localSocialTLFilterSwitchStore.value = 'withReplies';
 | 
				
			||||||
 | 
						} else if (onlyFilesTo) {
 | 
				
			||||||
 | 
							localSocialTLFilterSwitchStore.value = 'onlyFiles';
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							localSocialTLFilterSwitchStore.value = false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const withSensitive = computed({
 | 
					const withSensitive = computed({
 | 
				
			||||||
	get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
 | 
						get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
 | 
				
			||||||
	set: (x: boolean) => {
 | 
						set: (x: boolean) => saveTlFilter('withSensitive', x),
 | 
				
			||||||
		saveTlFilter('withSensitive', x);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// これだけはクライアント側で完結する処理なので手動でリロード
 | 
					 | 
				
			||||||
		tlComponent.value?.reloadTimeline();
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(src, () => {
 | 
					watch(src, () => {
 | 
				
			||||||
	queue.value = 0;
 | 
						queue.value = 0;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(withSensitive, () => {
 | 
				
			||||||
 | 
						// これだけはクライアント側で完結する処理なので手動でリロード
 | 
				
			||||||
 | 
						tlComponent.value?.reloadTimeline();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function queueUpdated(q: number): void {
 | 
					function queueUpdated(q: number): void {
 | 
				
			||||||
	queue.value = q;
 | 
						queue.value = q;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -184,10 +200,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
 | 
					function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
 | 
				
			||||||
	const out = {
 | 
						const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
 | 
				
			||||||
		...defaultStore.state.tl,
 | 
					 | 
				
			||||||
		src: newSrc,
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (newSrc.startsWith('userList:')) {
 | 
						if (newSrc.startsWith('userList:')) {
 | 
				
			||||||
		const id = newSrc.substring('userList:'.length);
 | 
							const id = newSrc.substring('userList:'.length);
 | 
				
			||||||
@@ -200,20 +213,9 @@ function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
 | 
					function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
 | 
				
			||||||
	if (key !== 'withReplies' || $i) {
 | 
						if (key !== 'withReplies' || $i) {
 | 
				
			||||||
		const out = { ...defaultStore.state.tl };
 | 
							const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
 | 
				
			||||||
		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
					 | 
				
			||||||
		if (!out.filter) {
 | 
					 | 
				
			||||||
			out.filter = {
 | 
					 | 
				
			||||||
				withRenotes: true,
 | 
					 | 
				
			||||||
				withReplies: true,
 | 
					 | 
				
			||||||
				withSensitive: true,
 | 
					 | 
				
			||||||
				onlyFiles: false,
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		out.filter[key] = newValue;
 | 
					 | 
				
			||||||
		defaultStore.set('tl', out);
 | 
							defaultStore.set('tl', out);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return newValue;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function timetravel(): Promise<void> {
 | 
					async function timetravel(): Promise<void> {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,10 @@
 | 
				
			|||||||
import { deepClone } from './clone.js';
 | 
					import { deepClone } from './clone.js';
 | 
				
			||||||
import type { Cloneable } from './clone.js';
 | 
					import type { Cloneable } from './clone.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type DeepPartial<T> = {
 | 
				
			||||||
 | 
						[P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
 | 
					function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
 | 
				
			||||||
	return typeof value === 'object' && value !== null && !Array.isArray(value);
 | 
						return typeof value === 'object' && value !== null && !Array.isArray(value);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -14,18 +18,18 @@ function isPureObject(value: unknown): value is Record<string | number | symbol,
 | 
				
			|||||||
 * valueにないキーをdefからもらう(再帰的)\
 | 
					 * valueにないキーをdefからもらう(再帰的)\
 | 
				
			||||||
 * nullはそのまま、undefinedはdefの値
 | 
					 * nullはそのまま、undefinedはdefの値
 | 
				
			||||||
 **/
 | 
					 **/
 | 
				
			||||||
export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: X, def: X): X {
 | 
					export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: DeepPartial<X>, def: X): X {
 | 
				
			||||||
	if (isPureObject(value) && isPureObject(def)) {
 | 
						if (isPureObject(value) && isPureObject(def)) {
 | 
				
			||||||
		const result = deepClone(value as Cloneable) as X;
 | 
							const result = deepClone(value as Cloneable) as X;
 | 
				
			||||||
		for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
 | 
							for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
 | 
				
			||||||
			if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
 | 
								if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
 | 
				
			||||||
				result[k] = v;
 | 
									result[k] = v;
 | 
				
			||||||
			} else if (isPureObject(v) && isPureObject(result[k])) {
 | 
								} else if (isPureObject(v) && isPureObject(result[k])) {
 | 
				
			||||||
				const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record<string | number | symbol, unknown>;
 | 
									const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<string | number | symbol, unknown>>;
 | 
				
			||||||
				result[k] = deepMerge<typeof v>(child, v);
 | 
									result[k] = deepMerge<typeof v>(child, v);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return result;
 | 
							return result;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return value;
 | 
						throw new Error('deepMerge: value and def must be pure objects');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user