fix(frontend): vue v3.4.16でタイムラインが正常に表示できない問題を修正
This commit is contained in:
parent
207e4f3b92
commit
92b2165828
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue