From 487a130f841ec0ec2546b6bc68d9b0701702cc44 Mon Sep 17 00:00:00 2001 From: Luca Pezzolla Date: Thu, 23 Nov 2023 13:20:58 +0100 Subject: [PATCH] refactor(preferences): allow to perform partial key update via update callback Refs #375 --- src/core/contexts/PreferencesContext.ts | 29 ++++++++-- src/core/providers/PreferencesProvider.tsx | 56 +++++++++++++------ .../screens/CourseIconPickerScreen.tsx | 4 +- src/features/user/screens/SettingsScreen.tsx | 6 +- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/core/contexts/PreferencesContext.ts b/src/core/contexts/PreferencesContext.ts index 76ca597a..c7d42037 100644 --- a/src/core/contexts/PreferencesContext.ts +++ b/src/core/contexts/PreferencesContext.ts @@ -42,8 +42,8 @@ export type CoursesPreferences = { [courseId: number | string]: CoursePreferencesProps; }; -export interface PreferencesContextBase { - lastInstalledVersion: string | null; +export interface EditablePreferences { + lastInstalledVersion?: string; username: string; campusId?: string; colorScheme: 'light' | 'dark' | 'system'; @@ -76,10 +76,27 @@ export interface PreferencesContextBase { }; } -export interface PreferencesContextProps extends PreferencesContextBase { - updatePreference: ( - key: T, - value: PreferencesContextBase[T], +/** + * A callback that receives the previous value of the preference and returns the next one + */ +type UpdatePreferenceCallback< + K extends PreferenceKey, + V extends EditablePreferences[K], +> = (prev: V | undefined) => V | undefined; + +/** + * The value of a preference can be: + * - a value of the type of the preference + * - a callback that receives the previous value of the preference and returns the next one + */ +export type UpdatePreferenceValue = + | EditablePreferences[K] + | UpdatePreferenceCallback; + +export interface PreferencesContextProps extends EditablePreferences { + updatePreference: ( + key: K, + value: UpdatePreferenceValue, ) => void; } diff --git a/src/core/providers/PreferencesProvider.tsx b/src/core/providers/PreferencesProvider.tsx index 1cc56121..a4236935 100644 --- a/src/core/providers/PreferencesProvider.tsx +++ b/src/core/providers/PreferencesProvider.tsx @@ -3,9 +3,11 @@ import { PropsWithChildren, useEffect, useRef, useState } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { + EditablePreferences, PreferenceKey, PreferencesContext, PreferencesContextProps, + UpdatePreferenceValue, editablePreferenceKeys, objectPreferenceKeys, } from '../contexts/PreferencesContext'; @@ -15,7 +17,7 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => { const deviceLanguage = useDeviceLanguage(); const [preferencesContext, setPreferencesContext] = useState({ - lastInstalledVersion: null, + lastInstalledVersion: undefined, username: '', colorScheme: 'system', courses: {}, @@ -38,35 +40,52 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => { const preferencesInitialized = useRef(false); - const updatePreference = (key: PreferenceKey, value: unknown) => { - const stringKey = key.toString(); - if (value === null) { - AsyncStorage.removeItem(stringKey).then(() => - setPreferencesContext(oldP => ({ - ...oldP, - [stringKey]: value, - })), - ); - } else { + // Initialize preferences from AsyncStorage + useEffect(() => { + const updatePreference = ( + key: K, + value: UpdatePreferenceValue, + ) => { + const stringKey = key.toString(); + + // if value is undefined, remove the preference + if (value === undefined) { + AsyncStorage.removeItem(stringKey).then(() => + setPreferencesContext(oldP => ({ + ...oldP, + [stringKey]: value, + })), + ); + + return; + } + let storageValue: string; + let nextValue: EditablePreferences[PreferenceKey]; + // if value is a callback, call it with the current value + if (typeof value === 'function') { + const currentValue = preferencesContext[key]; + nextValue = value(currentValue); + } else { + nextValue = value; + } + + // if value is an object, stringify it if (objectPreferenceKeys.includes(key)) { - storageValue = JSON.stringify(value); + storageValue = JSON.stringify(nextValue); } else { - storageValue = value as string; + storageValue = nextValue as string; } AsyncStorage.setItem(stringKey, storageValue).then(() => setPreferencesContext(oldP => ({ ...oldP, - [stringKey]: value, + [stringKey]: nextValue, })), ); - } - }; + }; - // Initialize preferences from AsyncStorage - useEffect(() => { AsyncStorage.multiGet(editablePreferenceKeys).then(storagePreferences => { const preferences: Partial = { updatePreference, @@ -90,6 +109,7 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => { return { ...oldP, ...preferences }; }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps -- only on mount }, []); // Preferences are loaded diff --git a/src/features/courses/screens/CourseIconPickerScreen.tsx b/src/features/courses/screens/CourseIconPickerScreen.tsx index 1c3d5080..3e292906 100644 --- a/src/features/courses/screens/CourseIconPickerScreen.tsx +++ b/src/features/courses/screens/CourseIconPickerScreen.tsx @@ -9,7 +9,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; import { - PreferencesContextBase, + EditablePreferences, usePreferencesContext, } from '../../../core/contexts/PreferencesContext'; import { useSafeAreaSpacing } from '../../../core/hooks/useSafeAreaSpacing'; @@ -96,7 +96,7 @@ export const CourseIconPickerScreen = ({ navigation, route }: Props) => { ...coursePrefs, icon: null, }, - } as PreferencesContextBase['courses']); + } as EditablePreferences['courses']); navigation.goBack(); }} /> diff --git a/src/features/user/screens/SettingsScreen.tsx b/src/features/user/screens/SettingsScreen.tsx index b3827ed3..9dab92b9 100644 --- a/src/features/user/screens/SettingsScreen.tsx +++ b/src/features/user/screens/SettingsScreen.tsx @@ -37,7 +37,7 @@ import { version } from '../../../../package.json'; import { BottomBarSpacer } from '../../../core/components/BottomBarSpacer'; import { useFeedbackContext } from '../../../core/contexts/FeedbackContext'; import { - PreferencesContextBase, + EditablePreferences, usePreferencesContext, } from '../../../core/contexts/PreferencesContext'; import { useConfirmationDialog } from '../../../core/hooks/useConfirmationDialog'; @@ -178,7 +178,7 @@ const VisualizationListItem = () => { onPressAction={({ nativeEvent: { event } }) => { updatePreference( 'colorScheme', - event as PreferencesContextBase['colorScheme'], + event as EditablePreferences['colorScheme'], ); }} > @@ -247,7 +247,7 @@ const Notifications = () => { updatePreference('notifications', { ...notifications, [notificationType]: value, - } as PreferencesContextBase['notifications']); + } as EditablePreferences['notifications']); }; return (