allow following-feed-utils to use alternate state backends

This commit is contained in:
Hazelnoot 2024-11-02 11:20:54 -04:00
parent 38787712d9
commit 38e30c0d54
1 changed files with 73 additions and 27 deletions

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed } from 'vue'; import { computed, Ref, WritableComputedRef } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { deepMerge } from '@/scripts/merge.js'; import { deepMerge } from '@/scripts/merge.js';
import { PageHeaderItem } from '@/types/page-header.js'; import { PageHeaderItem } from '@/types/page-header.js';
@ -13,9 +13,45 @@ import { popupMenu } from '@/os.js';
export const followingTab = 'following' as const; export const followingTab = 'following' as const;
export const mutualsTab = 'mutuals' as const; export const mutualsTab = 'mutuals' as const;
export const followersTab = 'followers' as const; export const followersTab = 'followers' as const;
export type FollowingFeedTab = typeof followingTab | typeof mutualsTab | typeof followersTab; export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const;
export type FollowingFeedTab = typeof followingFeedTabs[number];
export function createOptions(): PageHeaderItem { export function followingTabName(tab: FollowingFeedTab): string;
export function followingTabName(tab: FollowingFeedTab | null | undefined): null;
export function followingTabName(tab: FollowingFeedTab | null | undefined): string | null {
if (tab === followingTab) return i18n.ts.following;
if (tab === followersTab) return i18n.ts.followers;
if (tab === mutualsTab) return i18n.ts.mutuals;
return null;
}
export function followingTabIcon(tab: FollowingFeedTab | null | undefined): string {
if (tab === followersTab) return 'ph-user ph-bold ph-lg';
if (tab === mutualsTab) return 'ph-user-switch ph-bold ph-lg';
return 'ph-user-check ph-bold ph-lg';
}
export type FollowingFeedModel = {
[Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>;
}
export interface FollowingFeedState {
withNonPublic: boolean,
withQuotes: boolean,
withBots: boolean,
withReplies: boolean,
onlyFiles: boolean,
userList: FollowingFeedTab,
remoteWarningDismissed: boolean,
}
interface StorageInterface<T extends Partial<FollowingFeedState> = Partial<FollowingFeedState>> {
readonly state: Partial<T>;
readonly reactiveState: Ref<Partial<T>>;
save(updated: T): void;
}
export function createOptions(storage?: Ref<StorageInterface>): PageHeaderItem {
const { const {
userList, userList,
withNonPublic, withNonPublic,
@ -23,7 +59,7 @@ export function createOptions(): PageHeaderItem {
withBots, withBots,
withReplies, withReplies,
onlyFiles, onlyFiles,
} = createModel(); } = createModel(storage);
return { return {
icon: 'ti ti-dots', icon: 'ti ti-dots',
@ -62,41 +98,47 @@ export function createOptions(): PageHeaderItem {
disabled: withReplies, disabled: withReplies,
}, },
], ev.currentTarget ?? ev.target), ], ev.currentTarget ?? ev.target),
};
}
export function createModel() { export function createModel(storage?: Ref<StorageInterface>): FollowingFeedModel {
const userList = computed({ // eslint-disable-next-line no-param-reassign
get: () => defaultStore.reactiveState.followingFeed.value.userList, storage ??= createDefaultStorage();
// Based on timeline.saveTlFilter()
const saveFollowingFilter = <K extends keyof FollowingFeedState>(key: K, value: FollowingFeedState[K]) => {
const state = deepMerge(storage.value.state, defaultFollowingFeedState);
const out = deepMerge({ [key]: value }, state);
storage.value.save(out);
};
const userList: WritableComputedRef<FollowingFeedTab> = computed({
get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList,
set: value => saveFollowingFilter('userList', value), set: value => saveFollowingFilter('userList', value),
}); });
const withNonPublic: WritableComputedRef<boolean> = computed({
const withNonPublic = computed({
get: () => { get: () => {
if (userList.value === 'followers') return false; if (userList.value === 'followers') return false;
return defaultStore.reactiveState.followingFeed.value.withNonPublic; return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic;
}, },
set: value => saveFollowingFilter('withNonPublic', value), set: value => saveFollowingFilter('withNonPublic', value),
}); });
const withQuotes = computed({ const withQuotes: WritableComputedRef<boolean> = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes,
set: value => saveFollowingFilter('withQuotes', value), set: value => saveFollowingFilter('withQuotes', value),
}); });
const withBots = computed({ const withBots: WritableComputedRef<boolean> = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withBots, get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots,
set: value => saveFollowingFilter('withBots', value), set: value => saveFollowingFilter('withBots', value),
}); });
const withReplies = computed({ const withReplies: WritableComputedRef<boolean> = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withReplies, get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies,
set: value => saveFollowingFilter('withReplies', value), set: value => saveFollowingFilter('withReplies', value),
}); });
const onlyFiles = computed({ const onlyFiles: WritableComputedRef<boolean> = computed({
get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles, get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles,
set: value => saveFollowingFilter('onlyFiles', value), set: value => saveFollowingFilter('onlyFiles', value),
}); });
const remoteWarningDismissed: WritableComputedRef<boolean> = computed({
const remoteWarningDismissed = computed({ get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed,
get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed,
set: value => saveFollowingFilter('remoteWarningDismissed', value), set: value => saveFollowingFilter('remoteWarningDismissed', value),
}); });
@ -111,8 +153,12 @@ export function createModel() {
}; };
} }
// Based on timeline.saveTlFilter() function createDefaultStorage() {
function saveFollowingFilter<Key extends keyof typeof defaultStore.state.followingFeed>(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) { return computed(() => ({
const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed); state: defaultStore.state.followingFeed,
return defaultStore.set('followingFeed', out); reactiveState: defaultStore.reactiveState.followingFeed,
save(updated: typeof defaultStore.state.followingFeed) {
return defaultStore.set('followingFeed', updated);
},
}));
} }