diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-11-23 10:41:33 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-11-23 10:41:33 +0000 |
| commit | 6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7 (patch) | |
| tree | 307fcabbac6985abb8d59fa5c16ce021d1c1c7c4 /packages/frontend/src/scripts | |
| parent | fix some lints for frontend (diff) | |
| parent | merge: Move `cypress` to `optionalDependencies` (!697) (diff) | |
| download | sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.gz sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.bz2 sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.zip | |
Merge branch 'develop' into feature/2024.10
Diffstat (limited to 'packages/frontend/src/scripts')
| -rw-r--r-- | packages/frontend/src/scripts/following-feed-utils.ts | 189 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/merge.ts | 2 |
2 files changed, 127 insertions, 64 deletions
diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts index 064d6b72e3..39f17949d6 100644 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -3,19 +3,75 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed } from 'vue'; +import { computed, Ref, WritableComputedRef } from 'vue'; import { defaultStore } from '@/store.js'; import { deepMerge } from '@/scripts/merge.js'; import { PageHeaderItem } from '@/types/page-header.js'; import { i18n } from '@/i18n.js'; import { popupMenu } from '@/os.js'; +import { MenuItem } from '@/types/menu.js'; export const followingTab = 'following' as const; export const mutualsTab = 'mutuals' 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, +} + +export const defaultFollowingFeedState: FollowingFeedState = { + withNonPublic: false, + withQuotes: false, + withBots: true, + withReplies: false, + onlyFiles: false, + userList: followingTab, + remoteWarningDismissed: false, +}; + +interface StorageInterface<T extends Partial<FollowingFeedState> = Partial<FollowingFeedState>> { + readonly state: Partial<T>; + readonly reactiveState: Ref<Partial<T>>; + save(updated: T): void; +} + +export function createHeaderItem(storage?: Ref<StorageInterface>): PageHeaderItem { + const menu = createOptionsMenu(storage); + return { + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: ev => popupMenu(menu, ev.currentTarget ?? ev.target), + }; +} + +export function createOptionsMenu(storage?: Ref<StorageInterface>): MenuItem[] { const { userList, withNonPublic, @@ -23,80 +79,83 @@ export function createOptions(): PageHeaderItem { withBots, withReplies, onlyFiles, - } = createModel(); + } = createModel(storage); - return { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: ev => - popupMenu([ - { - type: 'switch', - text: i18n.ts.showNonPublicNotes, - ref: withNonPublic, - disabled: userList.value === 'followers', - }, - { - type: 'switch', - text: i18n.ts.showQuotes, - ref: withQuotes, - }, - { - type: 'switch', - text: i18n.ts.showBots, - ref: withBots, - }, - { - type: 'switch', - text: i18n.ts.showReplies, - ref: withReplies, - disabled: onlyFiles, - }, - { - type: 'divider', - }, - { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: withReplies, - }, - ], ev.currentTarget ?? ev.target), - }; + return [ + { + type: 'switch', + text: i18n.ts.showNonPublicNotes, + ref: withNonPublic, + disabled: computed(() => userList.value === followersTab), + }, + { + type: 'switch', + text: i18n.ts.showQuotes, + ref: withQuotes, + }, + { + type: 'switch', + text: i18n.ts.showBots, + ref: withBots, + }, + { + type: 'switch', + text: i18n.ts.showReplies, + ref: withReplies, + disabled: onlyFiles, + }, + { + type: 'divider', + }, + { + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: withReplies, + }, + ]; } -export function createModel() { - const userList = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.userList, +export function createModel(storage?: Ref<StorageInterface>): FollowingFeedModel { + // eslint-disable-next-line no-param-reassign + 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), }); - - const withNonPublic = computed({ + const withNonPublic: WritableComputedRef<boolean> = computed({ get: () => { 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), }); - const withQuotes = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, + const withQuotes: WritableComputedRef<boolean> = computed({ + get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, set: value => saveFollowingFilter('withQuotes', value), }); - const withBots = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withBots, + const withBots: WritableComputedRef<boolean> = computed({ + get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, set: value => saveFollowingFilter('withBots', value), }); - const withReplies = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withReplies, + const withReplies: WritableComputedRef<boolean> = computed({ + get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, set: value => saveFollowingFilter('withReplies', value), }); - const onlyFiles = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles, + const onlyFiles: WritableComputedRef<boolean> = computed({ + get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, set: value => saveFollowingFilter('onlyFiles', value), }); - - const remoteWarningDismissed = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed, + const remoteWarningDismissed: WritableComputedRef<boolean> = computed({ + get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, set: value => saveFollowingFilter('remoteWarningDismissed', value), }); @@ -111,8 +170,12 @@ export function createModel() { }; } -// Based on timeline.saveTlFilter() -function saveFollowingFilter<Key extends keyof typeof defaultStore.state.followingFeed>(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) { - const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed); - return defaultStore.set('followingFeed', out); +function createDefaultStorage() { + return computed(() => ({ + state: defaultStore.state.followingFeed, + reactiveState: defaultStore.reactiveState.followingFeed, + save(updated: typeof defaultStore.state.followingFeed) { + return defaultStore.set('followingFeed', updated); + }, + })); } diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 9794a300da..89fdda0cbb 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -18,7 +18,7 @@ function isPureObject(value: unknown): value is Record<string | number | symbol, * valueにないキーをdefからもらう(再帰的)\ * nullはそのまま、undefinedはdefの値 **/ -export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: DeepPartial<X>, def: X): X { +export function deepMerge<X extends object>(value: DeepPartial<X>, 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]][]) { |