+
{{ i18n.ts._timelineDescription[src] }}
@@ -50,6 +50,7 @@ import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache, userListsCache } from '@/cache.js';
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';
@@ -74,10 +75,14 @@ const withRenotes = computed({
get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
set: (x: boolean) => saveTlFilter('withRenotes', x),
});
-const withReplies = computed({
+
+// computed内での無限ループを防ぐためのフラグ
+const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies');
+
+const withReplies = computed({
get: () => {
if (!$i) return false;
- if (['local', 'social'].includes(src.value) && onlyFiles.value) {
+ if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
return false;
} else {
return defaultStore.reactiveState.tl.value.filter.withReplies;
@@ -85,9 +90,9 @@ const withReplies = computed({
},
set: (x: boolean) => saveTlFilter('withReplies', x),
});
-const onlyFiles = computed({
+const onlyFiles = computed({
get: () => {
- if (['local', 'social'].includes(src.value) && withReplies.value) {
+ if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
return false;
} else {
return defaultStore.reactiveState.tl.value.filter.onlyFiles;
@@ -95,20 +100,31 @@ const onlyFiles = computed({
},
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({
get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
- set: (x: boolean) => {
- saveTlFilter('withSensitive', x);
-
- // これだけはクライアント側で完結する処理なので手動でリロード
- tlComponent.value?.reloadTimeline();
- },
+ set: (x: boolean) => saveTlFilter('withSensitive', x),
});
watch(src, () => {
queue.value = 0;
});
+watch(withSensitive, () => {
+ // これだけはクライアント側で完結する処理なので手動でリロード
+ tlComponent.value?.reloadTimeline();
+});
+
function queueUpdated(q: number): void {
queue.value = q;
}
@@ -184,10 +200,7 @@ async function chooseChannel(ev: MouseEvent): Promise {
}
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
- const out = {
- ...defaultStore.state.tl,
- src: newSrc,
- };
+ const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
if (newSrc.startsWith('userList:')) {
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) {
if (key !== 'withReplies' || $i) {
- const out = { ...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;
+ const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
defaultStore.set('tl', out);
}
- return newValue;
}
async function timetravel(): Promise {
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
index 60097051fa..9fd87a9428 100644
--- a/packages/frontend/src/scripts/merge.ts
+++ b/packages/frontend/src/scripts/merge.ts
@@ -6,6 +6,10 @@
import { deepClone } from './clone.js';
import type { Cloneable } from './clone.js';
+type DeepPartial = {
+ [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P];
+};
+
function isPureObject(value: unknown): value is Record {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -14,18 +18,18 @@ function isPureObject(value: unknown): value is Record>(value: X, def: X): X {
+export function deepMerge>(value: DeepPartial, def: X): X {
if (isPureObject(value) && isPureObject(def)) {
const result = deepClone(value as Cloneable) as 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) {
result[k] = v;
} else if (isPureObject(v) && isPureObject(result[k])) {
- const child = deepClone(result[k] as Cloneable) as X[keyof X] & Record;
+ const child = deepClone(result[k] as Cloneable) as DeepPartial>;
result[k] = deepMerge(child, v);
}
}
return result;
}
- return value;
+ throw new Error('deepMerge: value and def must be pure objects');
}