summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-11-23 10:41:33 +0000
committerdakkar <dakkar@thenautilus.net>2024-11-23 10:41:33 +0000
commit6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7 (patch)
tree307fcabbac6985abb8d59fa5c16ce021d1c1c7c4 /packages/frontend/src/scripts
parentfix some lints for frontend (diff)
parentmerge: Move `cypress` to `optionalDependencies` (!697) (diff)
downloadsharkey-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.ts189
-rw-r--r--packages/frontend/src/scripts/merge.ts2
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]][]) {