From 54e56251ccf6ac0bdf2240862c8467d9a31c2b99 Mon Sep 17 00:00:00 2001 From: noodleofdeath Date: Mon, 9 Oct 2023 15:48:10 -0400 Subject: [PATCH] mob: refactor --- .../contexts/session/SessionContext.tsx | 167 +++++++++--------- src/core/src/client/contexts/session/types.ts | 69 +++++--- src/mobile/src/LeftDrawerScreen.tsx | 2 +- src/mobile/src/NavigationController.tsx | 6 +- src/mobile/src/RightDrawerScreen.tsx | 6 +- src/mobile/src/components/common/Header.tsx | 4 +- .../src/components/common/RoutedScreen.tsx | 6 +- .../src/components/common/SearchMenu.tsx | 4 +- .../src/components/dialogs/FeedbackDialog.tsx | 6 +- .../components/pickers/ColorSchemePicker.tsx | 6 +- .../src/components/pickers/FontPicker.tsx | 8 +- .../components/pickers/NumericPrefPicker.tsx | 2 +- .../src/components/pickers/PrefSwitch.tsx | 2 +- .../pickers/ReadingFormatPicker.tsx | 4 +- .../pickers/ShortPressFormatPicker.tsx | 4 +- .../components/pickers/TriggerWordPicker.tsx | 6 +- .../src/components/tables/SettingsTable.tsx | 16 +- .../walkthroughs/AppearanceWalkthrough.tsx | 14 +- .../walkthroughs/BookmarkWalkthrough.tsx | 10 +- .../walkthroughs/CustomFeedWalkthrough.tsx | 16 +- .../walkthroughs/SentimentWalkthrough.tsx | 14 +- .../walkthroughs/SharingWalkthrough.tsx | 10 +- .../walkthroughs/TemplateWalkthrough.tsx | 10 +- .../walkthroughs/TriggerWordsWalkthrough.tsx | 10 +- .../walkthroughs/WhatsNewWalkthrough.tsx | 10 +- .../notifications/NotificationContext.tsx | 24 +-- src/mobile/src/hooks/useNavigation.tsx | 13 +- src/mobile/src/screens/BookmarksScreen.tsx | 4 +- src/mobile/src/screens/StatsScreen.tsx | 4 +- .../screens/settings/CategoryPickerScreen.tsx | 8 +- .../settings/PublisherPickerScreen.tsx | 8 +- .../settings/ReadingFormatPickerScreen.tsx | 4 +- src/mobile/src/utils/usePlatformTools.ts | 6 +- 33 files changed, 255 insertions(+), 228 deletions(-) diff --git a/src/core/src/client/contexts/session/SessionContext.tsx b/src/core/src/client/contexts/session/SessionContext.tsx index 0ebb05545..5fd47dc17 100644 --- a/src/core/src/client/contexts/session/SessionContext.tsx +++ b/src/core/src/client/contexts/session/SessionContext.tsx @@ -1,14 +1,15 @@ import React from 'react'; import { - Bookmark, ColorScheme, DEFAULT_SESSION_CONTEXT, FunctionWithRequestParams, OrientationType, - PREFERENCE_TYPES, - Preferences, PushNotificationSettings, + STORED_VALUE_TYPES, + StoredValues, + TimelineEvent, + UserStats, } from './types'; import { @@ -36,7 +37,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const [latestVersion, setLatestVersion] = React.useState(); const [rotationLock, setRotationLock] = React.useState(); const [searchHistory, setSearchHistory] = React.useState(); - const [viewedFeatures, setViewedFeatures] = React.useState<{ [key: string]: Bookmark}>(); + const [viewedFeatures, setViewedFeatures] = React.useState<{ [key: string]: TimelineEvent}>(); const [hasReviewed, setHasReviewed] = React.useState(); const [lastRequestForReview, setLastRequestForReview] = React.useState(0); const [categories, setCategories] = React.useState>(); @@ -48,12 +49,13 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const [pushNotificationsEnabled, setPushNotificationsEnabled] = React.useState(); const [pushNotifications, setPushNotifications] = React.useState<{[key: string]: PushNotificationSettings}>(); const [fcmToken, setFcmToken] = React.useState(); + const [userStats, setUserStats] = React.useState(); // summary state - const [bookmarkedSummaries, setBookmarkedSummaries] = React.useState<{ [key: number]: Bookmark }>(); - const [readSummaries, setReadSummaries] = React.useState<{ [key: number]: Bookmark }>(); + const [bookmarkedSummaries, setBookmarkedSummaries] = React.useState<{ [key: number]: TimelineEvent }>(); + const [readSummaries, setReadSummaries] = React.useState<{ [key: number]: TimelineEvent }>(); const [removedSummaries, setRemovedSummaries] = React.useState<{ [key: number]: boolean }>(); - const [locale, setLocale] = React.useState(); + const [_, setLocale] = React.useState(); const [summaryTranslations, setSummaryTranslations] = React.useState<{ [key: number]: { [key in keyof PublicSummaryGroup]?: string } }>(); // bookmark state @@ -119,15 +121,15 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { // system functions - const getPreference = async (key: K): Promise => { + const getStoredValue = async (key: K): Promise => { const value = await getItem(key); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const serialize = (key: K, value: Preferences[K], type: 'boolean' | 'number' | 'string' | 'array' | 'object') => { + const serialize = (key: K, value: StoredValues[K], type: 'boolean' | 'number' | 'string' | 'array' | 'object') => { const isCorrectType = type === 'array' ? Array.isArray(value) : typeof value === type; if (!isCorrectType) { - setPreference(key, undefined, false); + setStoredValue(key, undefined, false); return undefined; } return value; @@ -135,7 +137,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { if (value) { try { - return serialize(key, JSON.parse(value), PREFERENCE_TYPES[key]); + return serialize(key, JSON.parse(value), STORED_VALUE_TYPES[key]); } catch (e) { return undefined; } @@ -143,10 +145,10 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { return undefined; }; - const setPreference = async (Preferences[K] | undefined))>(key: K, value?: V, emit = true) => { + const setStoredValue = async (StoredValues[K] | undefined))>(key: K, value?: V, emit = true) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newValue = (value instanceof Function ? value(await getPreference(key)) : value) as any; + const newValue = (value instanceof Function ? value(await getStoredValue(key)) : value) as any; switch (key) { @@ -169,9 +171,6 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { case 'lastRequestForReview': setLastRequestForReview(newValue); break; - case 'loadedInitialUrl': - setLoadedInitialUrl(newValue); - break; // user state case 'uuid': @@ -186,6 +185,9 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { case 'fcmToken': setFcmToken(newValue); break; + case 'userStat': + setUserStats(newValue); + break; // summary state case 'bookmarkedSummaries': @@ -284,10 +286,10 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const storeTranslations = async < Target extends RecapAttributes | PublicSummaryGroup, - PrefKey extends Target extends RecapAttributes ? 'recapTranslations' : Target extends PublicSummaryGroup ? 'summaryTranslations' : never, - State extends NonNullable, - >(item: Target, translations: { [key in keyof Target]?: string }, prefKey: PrefKey) => { - await setPreference(prefKey, (prev) => { + StoredValueKey extends Target extends RecapAttributes ? 'recapTranslations' : Target extends PublicSummaryGroup ? 'summaryTranslations' : never, + State extends NonNullable, + >(item: Target, translations: { [key in keyof Target]?: string }, prefKey: StoredValueKey) => { + await setStoredValue(prefKey, (prev) => { const state = { ...prev } as State; state[item.id] = translations; return (prev = state); @@ -315,7 +317,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { }, [pushNotifications]); const enablePush = async (type: string, settings?: PushNotificationSettings) => { - await setPreference('pushNotifications', (prev) => { + await setStoredValue('pushNotifications', (prev) => { const newState = { ...prev }; if (settings) { newState[type] = settings; @@ -331,10 +333,10 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { }, [viewedFeatures]); const viewFeature = async (feature: string, state = true) => { - await setPreference('viewedFeatures', (prev) => { + await setStoredValue('viewedFeatures', (prev) => { const newState = { ...prev }; if (state) { - newState[feature] = new Bookmark(true); + newState[feature] = new TimelineEvent(true); } else { delete newState[feature]; } @@ -345,13 +347,13 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { // summary functions const bookmarkSummary = async (summary: PublicSummaryGroup) => { - await setPreference('bookmarkedSummaries', (prev) => { + await setStoredValue('bookmarkedSummaries', (prev) => { const state = { ...prev }; if (summary.id in state) { delete state[summary.id]; emitEvent('unbookmark-summary', summary, state); } else { - state[summary.id] = new Bookmark(summary); + state[summary.id] = new TimelineEvent(summary); viewFeature('unread-bookmarks', false); emitEvent('bookmark-summary', summary, state); } @@ -360,13 +362,13 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { }; const readSummary = async (summary: PublicSummaryGroup, force = false) => { - await setPreference('readSummaries', (prev) => { + await setStoredValue('readSummaries', (prev) => { const state = { ...prev }; if (force && summary.id in state) { delete state[summary.id]; emitEvent('unread-summary', summary, state); } else { - state[summary.id] = new Bookmark(true); + state[summary.id] = new TimelineEvent(true); emitEvent('read-summary', summary, state); } return (prev = state); @@ -374,7 +376,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { }; const removeSummary = async (summary: PublicSummaryGroup) => { - await setPreference('removedSummaries', (prev) => { + await setStoredValue('removedSummaries', (prev) => { const state = { ...prev }; if (summary.id in state) { delete state[summary.id]; @@ -390,7 +392,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { // recap functions const readRecap = async (recap: RecapAttributes, force = false) => { - await setPreference('readRecaps', (prev) => { + await setStoredValue('readRecaps', (prev) => { const state = { ...prev }; if (force && recap.id in state) { delete state[recap.id]; @@ -406,11 +408,11 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { // publisher functions const followPublisher = async (publisher: PublicPublisherAttributes) => { - await setPreference('followedPublishers', (prev) => { + await setStoredValue('followedPublishers', (prev) => { const state = { ...prev }; if (publisher.name in state) { delete state[publisher.name]; - setPreference('favoritedPublishers', (prev) => { + setStoredValue('favoritedPublishers', (prev) => { const state = { ...prev }; delete state[publisher.name]; return (prev = state); @@ -418,7 +420,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { emitEvent('unfollow-publisher', publisher, state); } else { state[publisher.name] = true; - setPreference('excludedPublishers', (prev) => { + setStoredValue('excludedPublishers', (prev) => { const state = { ...prev }; delete state[publisher.name]; return (prev = state); @@ -432,7 +434,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const isFollowingPublisher = React.useCallback((publisher: PublicPublisherAttributes) => publisher.name in ({ ...followedPublishers }), [followedPublishers]); const favoritePublisher = async (publisher: PublicPublisherAttributes) => { - await setPreference('favoritedPublishers', (prev) => { + await setStoredValue('favoritedPublishers', (prev) => { const state = { ...prev }; if (publisher.name in state) { delete state[publisher.name]; @@ -448,14 +450,14 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const publisherIsFavorited = React.useCallback((publisher: PublicPublisherAttributes) => publisher.name in ({ ...favoritedPublishers }), [favoritedPublishers]); const excludePublisher = async (publisher: PublicPublisherAttributes) => { - await setPreference('excludedPublishers', (prev) => { + await setStoredValue('excludedPublishers', (prev) => { const state = { ...prev }; if (publisher.name in state) { delete state[publisher.name]; emitEvent('unexclude-publisher', publisher, state); } else { state[publisher.name] = true; - setPreference('followedPublishers', (prev) => { + setStoredValue('followedPublishers', (prev) => { const state = { ...prev }; delete state[publisher.name]; return (prev = state); @@ -471,11 +473,11 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { // category functions const followCategory = async (category: PublicCategoryAttributes) => { - await setPreference('followedCategories', (prev) => { + await setStoredValue('followedCategories', (prev) => { const state = { ...prev }; if (category.name in state) { delete state[category.name]; - setPreference('followedCategories', (prev) => { + setStoredValue('followedCategories', (prev) => { const state = { ...prev }; delete state[category.name]; return (prev = state); @@ -483,7 +485,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { emitEvent('unfollow-category', category, state); } else { state[category.name] = true; - setPreference('excludedCategories', (prev) => { + setStoredValue('excludedCategories', (prev) => { const state = { ...prev }; delete state[category.name]; return (prev = state); @@ -497,7 +499,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const isFollowingCategory = React.useCallback((category: PublicCategoryAttributes) => category.name in ({ ...followedCategories }), [followedCategories]); const favoriteCategory = async (category: PublicCategoryAttributes) => { - await setPreference('favoritedCategories', (prev) => { + await setStoredValue('favoritedCategories', (prev) => { const state = { ...prev }; if (category.name in state) { delete state[category.name]; @@ -513,14 +515,14 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { const categoryIsFavorited = React.useCallback((category: PublicCategoryAttributes) => category.name in ({ ...favoritedCategories }), [favoritedCategories]); const excludeCategory = async (category: PublicCategoryAttributes) => { - await setPreference('excludedCategories', (prev) => { + await setStoredValue('excludedCategories', (prev) => { const state = { ...prev }; if (category.name in state) { delete state[category.name]; emitEvent('unexclude-category', category, state); } else { state[category.name] = true; - setPreference('followedCategories', (prev) => { + setStoredValue('followedCategories', (prev) => { const state = { ...prev }; delete state[category.name]; return (prev = state); @@ -536,58 +538,62 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { // Load preferences on mount const load = async () => { // system state - setLatestVersion(await getPreference('latestVersion')); - setRotationLock(await getPreference('rotationLock')); - setSearchHistory(await getPreference('searchHistory')); - setViewedFeatures(await getPreference('viewedFeatures')); - setHasReviewed(await getPreference('hasReviewed')); - setLastRequestForReview(await getPreference('lastRequestForReview') ?? 0); - setUuid(await getPreference('uuid')); - setPushNotificationsEnabled(await getPreference('pushNotificationsEnabled')); - setPushNotifications(await getPreference('pushNotifications')); - setFcmToken(await getPreference('fcmToken')); + setLatestVersion(await getStoredValue('latestVersion')); + setRotationLock(await getStoredValue('rotationLock')); + setSearchHistory(await getStoredValue('searchHistory')); + setViewedFeatures(await getStoredValue('viewedFeatures')); + setHasReviewed(await getStoredValue('hasReviewed')); + setLastRequestForReview(await getStoredValue('lastRequestForReview') ?? 0); + setUuid(await getStoredValue('uuid')); + setPushNotificationsEnabled(await getStoredValue('pushNotificationsEnabled')); + setPushNotifications(await getStoredValue('pushNotifications')); + setFcmToken(await getStoredValue('fcmToken')); + setUserStats(await getStoredValue('userStats')); // summary state - setBookmarkedSummaries(await getPreference('bookmarkedSummaries')); - setReadSummaries(await getPreference('readSummaries')); - setRemovedSummaries(await getPreference('removedSummaries')); - const locale = await getPreference('locale'); + setBookmarkedSummaries(await getStoredValue('bookmarkedSummaries')); + setReadSummaries(await getStoredValue('readSummaries')); + setRemovedSummaries(await getStoredValue('removedSummaries')); + + const locale = await getStoredValue('locale'); setLocale(locale); - setSummaryTranslations(locale !== getLocale() ? {} : await getPreference('summaryTranslations')); + setSummaryTranslations(locale !== getLocale() ? {} : await getStoredValue('summaryTranslations')); // recap state - setReadRecaps(await getPreference('readRecaps')); - setRecapTranslations(locale !== getLocale() ? {} : await getPreference('recapTranslations')); + setReadRecaps(await getStoredValue('readRecaps')); + setRecapTranslations(locale !== getLocale() ? {} : await getStoredValue('recapTranslations')); + + setLocale(getLocale()); // publisher states - setFollowedPublishers(await getPreference('followedPublishers')); - setFavoritedPublishers(await getPreference('favoritedPublishers')); - setExcludedPublishers(await getPreference('excludedPublishers')); + setFollowedPublishers(await getStoredValue('followedPublishers')); + setFavoritedPublishers(await getStoredValue('favoritedPublishers')); + setExcludedPublishers(await getStoredValue('excludedPublishers')); // category states - setFollowedCategories(await getPreference('followedCategories')); - setFavoritedCategories(await getPreference('favoritedCategories')); - setExcludedCategories(await getPreference('excludedCategories')); + setFollowedCategories(await getStoredValue('followedCategories')); + setFavoritedCategories(await getStoredValue('favoritedCategories')); + setExcludedCategories(await getStoredValue('excludedCategories')); // system preferences - setColorScheme(await getPreference('colorScheme')); - setFontFamily(await getPreference('fontFamily')); - setFontSizeOffset(await getPreference('fontSizeOffset')); - setLetterSpacing(await getPreference('letterSpacing')); - setLineHeightMultiplier(await getPreference('lineHeightMultiplier')); + setColorScheme(await getStoredValue('colorScheme')); + setFontFamily(await getStoredValue('fontFamily')); + setFontSizeOffset(await getStoredValue('fontSizeOffset')); + setLetterSpacing(await getStoredValue('letterSpacing')); + setLineHeightMultiplier(await getStoredValue('lineHeightMultiplier')); // summary preferences - setCompactSummaries(await getPreference('compactSummaries') ?? await getPreference('compactMode')); - setShowShortSummary(await getPreference('showShortSummary')); - setPreferredReadingFormat(await getPreference('preferredReadingFormat')); - setPreferredShortPressFormat(await getPreference('preferredShortPressFormat')); - setSentimentEnabled(await getPreference('sentimentEnabled')); - setTriggerWords(await getPreference('triggerWords')); + setCompactSummaries(await getStoredValue('compactSummaries') ?? await getStoredValue('compactMode')); + setShowShortSummary(await getStoredValue('showShortSummary')); + setPreferredReadingFormat(await getStoredValue('preferredReadingFormat')); + setPreferredShortPressFormat(await getStoredValue('preferredShortPressFormat')); + setSentimentEnabled(await getStoredValue('sentimentEnabled')); + setTriggerWords(await getStoredValue('triggerWords')); setReady(true); }; - const resetPreferences = async (hard = false) => { + const resetStoredValues = async (hard = false) => { await removeAll(hard); load(); }; @@ -625,7 +631,7 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { followedPublishers, fontFamily, fontSizeOffset, - getPreference, + getStoredValue, hasPushEnabled, hasReviewed, hasViewedFeature, @@ -652,18 +658,19 @@ export function SessionContextProvider({ children }: React.PropsWithChildren) { recapTranslations, removeSummary, removedSummaries, - resetPreferences, + resetStoredValues, rotationLock, searchHistory, sentimentEnabled, setCategories, - setPreference, - setPublishers, + setLoadedInitialUrl, setPublishers, + setStoredValue, showShortSummary, storeTranslations, summaryTranslations, triggerWords, unreadBookmarkCount, + userStats, uuid, viewFeature, viewedFeatures, diff --git a/src/core/src/client/contexts/session/types.ts b/src/core/src/client/contexts/session/types.ts index e8e2fa3ec..1490a0d2d 100644 --- a/src/core/src/client/contexts/session/types.ts +++ b/src/core/src/client/contexts/session/types.ts @@ -10,12 +10,12 @@ import { } from '~/api'; import { Locale } from '~/locales'; -export type BookmarkConstructorProps = { +export type TimelineEventProps = { createdAt: Date; expiresIn?: string; }; -export class Bookmark { +export class TimelineEvent { item: T; createdAt: Date; @@ -31,7 +31,7 @@ export class Bookmark { constructor(item: T, { createdAt = new Date(), expiresIn, - }: Partial = {}) { + }: Partial = {}) { this.item = item; this.createdAt = createdAt; if (expiresIn) { @@ -109,26 +109,26 @@ export type Resource = export type SessionEvent = Activity | ResourceActivity | `${ResourceActivity}-${Resource}` | `${ResourceActivity}-${Resource}-${number}` | `poll-${string}`; -export type Preferences = { +export type StoredValues = { // system state latestVersion?: string; rotationLock?: OrientationType; searchHistory?: string[]; - viewedFeatures?: { [key: string]: Bookmark }; + viewedFeatures?: { [key: string]: TimelineEvent }; hasReviewed?: boolean; lastRequestForReview: number; - loadedInitialUrl?: boolean; // user state uuid?: string; pushNotificationsEnabled?: boolean; pushNotifications?: { [key: string]: PushNotificationSettings }; fcmToken?: string; + userStats?: UserStats; // summary state - readSummaries?: { [key: number]: Bookmark }; - bookmarkedSummaries?: { [key: number]: Bookmark }; + readSummaries?: { [key: number]: TimelineEvent }; + bookmarkedSummaries?: { [key: number]: TimelineEvent }; bookmarkCount: number; unreadBookmarkCount: number; removedSummaries?: { [key: number]: boolean }; @@ -170,7 +170,7 @@ export type Preferences = { triggerWords?: { [key: string]: string }; }; -export const PREFERENCE_TYPES: { [key in keyof Preferences]: 'boolean' | 'number' | 'string' | 'object' | 'array' } = { +export const STORED_VALUE_TYPES: { [key in keyof StoredValues]: 'boolean' | 'number' | 'string' | 'object' | 'array' } = { bookmarkCount: 'number', bookmarkedSummaries: 'object', colorScheme: 'string', @@ -192,7 +192,6 @@ export const PREFERENCE_TYPES: { [key in keyof Preferences]: 'boolean' | 'number lastRequestForReview: 'number', letterSpacing: 'number', lineHeightMultiplier: 'number', - loadedInitialUrl: 'boolean', locale: 'string', preferredReadingFormat: 'string', preferredShortPressFormat: 'string', @@ -209,6 +208,7 @@ export const PREFERENCE_TYPES: { [key in keyof Preferences]: 'boolean' | 'number summaryTranslations: 'object', triggerWords: 'object', unreadBookmarkCount: 'number', + userStats: 'object', uuid: 'string', viewedFeatures: 'object', }; @@ -226,31 +226,47 @@ export type PreferenceMutation = any; export type PreferenceState = - E extends `${'unbookmark' | 'bookmark'}-summary` ? Preferences['bookmarkedSummaries'] : - E extends `${'read' | 'unread'}-summary` ? Preferences['readSummaries'] : - E extends `${'read' | 'unread'}-recap` ? Preferences['readRecaps'] : - E extends `${string}-summary` ? Preferences['removedSummaries'] : - E extends `${string}-publisher` ? Preferences['followedPublishers'] : - E extends `${string}-category` ? Preferences['followedCategories'] : + E extends `${'unbookmark' | 'bookmark'}-summary` ? StoredValues['bookmarkedSummaries'] : + E extends `${'read' | 'unread'}-summary` ? StoredValues['readSummaries'] : + E extends `${'read' | 'unread'}-recap` ? StoredValues['readRecaps'] : + E extends `${string}-summary` ? StoredValues['removedSummaries'] : + E extends `${string}-publisher` ? StoredValues['followedPublishers'] : + E extends `${string}-category` ? StoredValues['followedCategories'] : // eslint-disable-next-line @typescript-eslint/no-explicit-any any; -export type SessionContextType = Preferences & { +export type Streak = { + start: Date; + end: Date; + length: number; +}; + +export type UserStats = { + lastSeen?: Date; + streak?: Streak; + longestStreak?: Streak; +}; + +export type SessionContextType = StoredValues & { ready?: boolean; + loadedInitialUrl?: boolean; + setLoadedInitialUrl: React.Dispatch>; categories?: Record; setCategories: React.Dispatch | undefined>>; publishers?: Record; setPublishers: React.Dispatch | undefined>>; - + + userStats?: UserStats; + // state setters - setPreference: (Preferences[K] | undefined))>(key: K, value?: V, emit?: boolean) => Promise; - getPreference: (key: K) => Promise; - resetPreferences: (hard?: boolean) => Promise; + setStoredValue: (StoredValues[K] | undefined))>(key: K, value?: V, emit?: boolean) => Promise; + getStoredValue: (key: K) => Promise; + resetStoredValues: (hard?: boolean) => Promise; storeTranslations: < Target extends RecapAttributes | PublicSummaryGroup, - PrefKey extends Target extends RecapAttributes ? 'recapTranslations' : Target extends PublicSummaryGroup ? 'summaryTranslations' : never - >(item: Target, translations: { [key in keyof Target]?: string }, prefKey: PrefKey) => Promise; + StoredValueKey extends Target extends RecapAttributes ? 'recapTranslations' : Target extends PublicSummaryGroup ? 'summaryTranslations' : never + >(item: Target, translations: { [key in keyof Target]?: string }, prefKey: StoredValueKey) => Promise; hasPushEnabled: (key: string) => boolean; enablePush: (key: string, settings?: PushNotificationSettings) => Promise; hasViewedFeature: (...features: string[]) => boolean; @@ -297,7 +313,7 @@ export const DEFAULT_SESSION_CONTEXT: SessionContextType = { followCount: 0, followFilter: '', followPublisher: () => Promise.resolve(), - getPreference: () => Promise.resolve(undefined), + getStoredValue: () => Promise.resolve(undefined), hasPushEnabled: () => false, hasViewedFeature: () => false, isExcludingCategory: () => false, @@ -309,10 +325,11 @@ export const DEFAULT_SESSION_CONTEXT: SessionContextType = { readRecap: () => Promise.resolve(), readSummary: () => Promise.resolve(), removeSummary: () => Promise.resolve(), - resetPreferences: () => Promise.resolve(), + resetStoredValues: () => Promise.resolve(), setCategories: () => Promise.resolve(), - setPreference: () => Promise.resolve(), + setLoadedInitialUrl: () => Promise.resolve(), setPublishers: () => Promise.resolve(), + setStoredValue: () => Promise.resolve(), storeTranslations: () => Promise.resolve(undefined), unreadBookmarkCount: 0, viewFeature: () => Promise.resolve(), diff --git a/src/mobile/src/LeftDrawerScreen.tsx b/src/mobile/src/LeftDrawerScreen.tsx index e4432ff3b..745c81613 100644 --- a/src/mobile/src/LeftDrawerScreen.tsx +++ b/src/mobile/src/LeftDrawerScreen.tsx @@ -216,7 +216,7 @@ const LeftDrawer = createDrawerNavigator(); export function LeftDrawerScreen() { return ( ({ headerShown: false, diff --git a/src/mobile/src/NavigationController.tsx b/src/mobile/src/NavigationController.tsx index e52ffe593..90d84bf77 100644 --- a/src/mobile/src/NavigationController.tsx +++ b/src/mobile/src/NavigationController.tsx @@ -43,7 +43,7 @@ export default function NavigationController() { hasViewedFeature, lastRequestForReview = 0, readSummaries, - setPreference, + setStoredValue, } = React.useContext(SessionContext); const { isTablet, @@ -87,7 +87,7 @@ export default function NavigationController() { } setShowedReview(success); emitEvent(success ? 'in-app-review' : 'in-app-review-failed'); - setPreference('lastRequestForReview', Date.now()); + setStoredValue('lastRequestForReview', Date.now()); } catch (error) { console.error(error); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -111,7 +111,7 @@ export default function NavigationController() { }; } - }, [ready, isTablet, lockRotation, showedReview, lastRequestForReview, unlockRotation, readSummaries, setPreference, emitEvent]); + }, [ready, isTablet, lockRotation, showedReview, lastRequestForReview, unlockRotation, readSummaries, setStoredValue, emitEvent]); const refreshSources = React.useCallback(() => { if (lastFetchFailed || (Date.now() - lastFetch < ms('10s'))) { diff --git a/src/mobile/src/RightDrawerScreen.tsx b/src/mobile/src/RightDrawerScreen.tsx index bdc8c96dd..573b4b5b8 100644 --- a/src/mobile/src/RightDrawerScreen.tsx +++ b/src/mobile/src/RightDrawerScreen.tsx @@ -63,8 +63,8 @@ const RightDrawer = createDrawerNavigator(); export function RightDrawerScreen() { return ( ({ drawerPosition: 'right', headerShown: false, @@ -72,7 +72,7 @@ export function RightDrawerScreen() { }) } drawerContent={ (props) => }> ); diff --git a/src/mobile/src/components/common/Header.tsx b/src/mobile/src/components/common/Header.tsx index 8a198472d..db3ce0f62 100644 --- a/src/mobile/src/components/common/Header.tsx +++ b/src/mobile/src/components/common/Header.tsx @@ -19,7 +19,7 @@ export function DrawerToggle(props: ButtonProps) { accessible accessibilityLabel={ strings.axe_menu } onPress={ () => { - navigation?.getParent('LeftDrawer')?.openDrawer?.(); + (navigation?.getParent('leftDrawerNav') as typeof navigation)?.openDrawer?.(); } } { ...props } /> @@ -37,7 +37,7 @@ export function SettingsToggle(props: ButtonProps) { accessibilityLabel={ strings.axe_settings } iconSize={ 24 } onPress={ () => { - navigation?.getParent('RightDrawer')?.openDrawer?.(); + (navigation?.getParent('rightDrawerNav') as typeof navigation)?.openDrawer?.(); } } { ...props } /> diff --git a/src/mobile/src/components/common/RoutedScreen.tsx b/src/mobile/src/components/common/RoutedScreen.tsx index 22a42ac7a..66f7be5f7 100644 --- a/src/mobile/src/components/common/RoutedScreen.tsx +++ b/src/mobile/src/components/common/RoutedScreen.tsx @@ -10,20 +10,20 @@ import { useNavigation } from '~/hooks'; export function RoutedScreen({ ...props }: ScreenProps) { const { navigation, router } = useNavigation(); - const { loadedInitialUrl, setPreference } = React.useContext(SessionContext); + const { loadedInitialUrl, setLoadedInitialUrl } = React.useContext(SessionContext); useFocusEffect(React.useCallback(() => { const subscriber = Linking.addEventListener('url', router); if (!loadedInitialUrl) { Linking.getInitialURL().then((url) => { if (url) { - setPreference('loadedInitialUrl', true); + setLoadedInitialUrl(true); router({ stackNav: navigation?.getParent('stackNav'), url }); } }); } return () => subscriber.remove(); - }, [router, navigation, loadedInitialUrl, setPreference])); + }, [router, navigation, loadedInitialUrl, setLoadedInitialUrl])); return ( diff --git a/src/mobile/src/components/common/SearchMenu.tsx b/src/mobile/src/components/common/SearchMenu.tsx index b49331b8d..563322935 100644 --- a/src/mobile/src/components/common/SearchMenu.tsx +++ b/src/mobile/src/components/common/SearchMenu.tsx @@ -43,7 +43,7 @@ export function SearchMenu({ const { searchHistory, - setPreference, + setStoredValue, } = React.useContext(SessionContext); const { screenWidth } = React.useContext(LayoutContext); @@ -102,7 +102,7 @@ export function SearchMenu({ - diff --git a/src/mobile/src/components/dialogs/FeedbackDialog.tsx b/src/mobile/src/components/dialogs/FeedbackDialog.tsx index be2cc8c9d..138e4b913 100644 --- a/src/mobile/src/components/dialogs/FeedbackDialog.tsx +++ b/src/mobile/src/components/dialogs/FeedbackDialog.tsx @@ -24,7 +24,7 @@ export function FeedbackDialog({ payload, ...props }: SheetProps ({ ...payload }), [payload]); - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const { interactWithSummary } = useApiClient(); @@ -65,7 +65,7 @@ export function FeedbackDialog({ payload, ...props }: SheetProps { + setStoredValue('removedSummaries', (prev) => { const summaries = { ...prev }; if (summaries[summary.id]) { delete summaries[summary.id]; @@ -84,7 +84,7 @@ export function FeedbackDialog({ payload, ...props }: SheetProps diff --git a/src/mobile/src/components/pickers/ColorSchemePicker.tsx b/src/mobile/src/components/pickers/ColorSchemePicker.tsx index 24a94ded9..be711e7a2 100644 --- a/src/mobile/src/components/pickers/ColorSchemePicker.tsx +++ b/src/mobile/src/components/pickers/ColorSchemePicker.tsx @@ -21,7 +21,7 @@ export function ColorSchemePicker({ ...props }: ColorSchemePickerProps) { - const { colorScheme, setPreference } = React.useContext(SessionContext); + const { colorScheme, setStoredValue } = React.useContext(SessionContext); if (variant === 'table') { return ( @@ -33,7 +33,7 @@ export function ColorSchemePicker({ ] } initialValue={ colorScheme ?? 'system' } onValueChange={ (colorScheme) => { - setPreference('colorScheme', colorScheme); + setStoredValue('colorScheme', colorScheme); } }> setPreference('colorScheme', value) } + onValueChange={ (value) => setStoredValue('colorScheme', value) } options={ [ { label: strings.settings_light, value: 'light' as ColorScheme }, { label: strings.settings_system, value: 'system' as ColorScheme }, diff --git a/src/mobile/src/components/pickers/FontPicker.tsx b/src/mobile/src/components/pickers/FontPicker.tsx index b5e7a0cec..46479cce7 100644 --- a/src/mobile/src/components/pickers/FontPicker.tsx +++ b/src/mobile/src/components/pickers/FontPicker.tsx @@ -28,7 +28,7 @@ export function FontPicker({ ...props }: FontPickerProps = {}) { - const { fontFamily = DEFAULT_PREFERRED_FONT, setPreference } = React.useContext(SessionContext); + const { fontFamily = DEFAULT_PREFERRED_FONT, setStoredValue } = React.useContext(SessionContext); const style = useStyles(props); if (variant === 'grid') { @@ -38,7 +38,7 @@ export function FontPicker({ initialValue={ fontFamily as FontFamily } buttonProps={ ({ option }) => ({ fontFamily: option.value }) } onValueChange={ (font) => { - setPreference('fontFamily', font); + setStoredValue('fontFamily', font); } } /> ); } else @@ -59,7 +59,7 @@ export function FontPicker({ leftIcon={ fontFamily === font ? 'check' : undefined } fontFamily={ font } onPress={ () => { - setPreference('fontFamily', font); + setStoredValue('fontFamily', font); } }> {font} @@ -74,7 +74,7 @@ export function FontPicker({ options={ [...AVAILABLE_FONTS] } initialValue={ fontFamily as FontFamily } cellProps={ ({ option }) => ({ titleTextStyle: { fontFamily: option.value } }) } - onValueChange={ (font) => setPreference('fontFamily', font) }> + onValueChange={ (font) => setStoredValue('fontFamily', font) }> ({ } } setValue(newValue); - context.setPreference(prefKey, newValue); + context.setStoredValue(prefKey, newValue); }, [min, max, context, prefKey]); return ( diff --git a/src/mobile/src/components/pickers/PrefSwitch.tsx b/src/mobile/src/components/pickers/PrefSwitch.tsx index c47a39a04..74841dfbe 100644 --- a/src/mobile/src/components/pickers/PrefSwitch.tsx +++ b/src/mobile/src/components/pickers/PrefSwitch.tsx @@ -26,7 +26,7 @@ export function PrefSwitch({ value={ value } onValueChange={ (value) => { setValue(value); - context.setPreference(prefKey, value); + context.setStoredValue(prefKey, value); onValueChange?.(value); } } { ...props } /> diff --git a/src/mobile/src/components/pickers/ReadingFormatPicker.tsx b/src/mobile/src/components/pickers/ReadingFormatPicker.tsx index 62ccef920..5644d2293 100644 --- a/src/mobile/src/components/pickers/ReadingFormatPicker.tsx +++ b/src/mobile/src/components/pickers/ReadingFormatPicker.tsx @@ -28,7 +28,7 @@ export function ReadingFormatPicker({ pressOnly, ...props }: Props = {}) { - const { preferredReadingFormat, setPreference } = React.useContext(SessionContext); + const { preferredReadingFormat, setStoredValue } = React.useContext(SessionContext); const buttonsRef = React.useRef>(null); return ( [ { onPress: () => { - setPreference('preferredReadingFormat', option.value); + setStoredValue('preferredReadingFormat', option.value); if (option.value !== ReadingFormat.FullArticle) { buttonsRef.current?.setValue(option.value); } diff --git a/src/mobile/src/components/pickers/ShortPressFormatPicker.tsx b/src/mobile/src/components/pickers/ShortPressFormatPicker.tsx index 3ef61c714..ee4adc202 100644 --- a/src/mobile/src/components/pickers/ShortPressFormatPicker.tsx +++ b/src/mobile/src/components/pickers/ShortPressFormatPicker.tsx @@ -21,14 +21,14 @@ const OPTIONS: SelectOption[] = [{ }]; export function ShortPressFormatPicker({ ...props }: Props = {}) { - const { preferredShortPressFormat, setPreference } = React.useContext(SessionContext); + const { preferredShortPressFormat, setStoredValue } = React.useContext(SessionContext); return ( { - setPreference('preferredShortPressFormat', value); + setStoredValue('preferredShortPressFormat', value); } }> ); diff --git a/src/mobile/src/components/pickers/TriggerWordPicker.tsx b/src/mobile/src/components/pickers/TriggerWordPicker.tsx index d74b88dba..02e60ee10 100644 --- a/src/mobile/src/components/pickers/TriggerWordPicker.tsx +++ b/src/mobile/src/components/pickers/TriggerWordPicker.tsx @@ -20,14 +20,14 @@ export function TriggerWordPicker({ saveLabel = strings.action_save, }: TriggerWordPickerProps) { - const { triggerWords, setPreference } = React.useContext(SessionContext); + const { triggerWords, setStoredValue } = React.useContext(SessionContext); const [words, setWords] = React.useState([...Object.entries({ ...triggerWords }).map(([word, replacement]) => [word, replacement]), ['', '']]); const save = React.useCallback(() => { - setPreference('triggerWords', Object.fromEntries(words.map(([word, replacement]) => [word.toLowerCase().replace(/[\n\s\t]+/g, ' '), replacement]).filter((pair) => !/^\s*$/.test(pair.join(''))))); + setStoredValue('triggerWords', Object.fromEntries(words.map(([word, replacement]) => [word.toLowerCase().replace(/[\n\s\t]+/g, ' '), replacement]).filter((pair) => !/^\s*$/.test(pair.join(''))))); onSubmit?.(); - }, [setPreference, words, onSubmit]); + }, [setStoredValue, words, onSubmit]); return ( diff --git a/src/mobile/src/components/tables/SettingsTable.tsx b/src/mobile/src/components/tables/SettingsTable.tsx index 5b868ffcc..0b96773e3 100644 --- a/src/mobile/src/components/tables/SettingsTable.tsx +++ b/src/mobile/src/components/tables/SettingsTable.tsx @@ -31,11 +31,11 @@ export function SettingsTable() { fontFamily, preferredShortPressFormat, preferredReadingFormat, - resetPreferences, + resetStoredValues, triggerWords, readSummaries, removedSummaries, - setPreference, + setStoredValue, } = React.useContext(SessionContext); const [loading, setLoading] = React.useState(false); @@ -175,7 +175,7 @@ export function SettingsTable() { bold title={ `${strings.settings_resetReadSummaries} (${Object.keys({ ...readSummaries }).length})` } onPress={ () => { - setPreference('readSummaries', {}); + setStoredValue('readSummaries', {}); } } onLongPress={ () => { navigate('test'); @@ -184,9 +184,9 @@ export function SettingsTable() { bold title={ `${strings.settings_resetHiddenSummaries} (${Object.keys({ ...removedSummaries }).length})` } onPress={ () => { - setPreference('removedSummaries', {}); - setPreference('excludedCategories', {}); - setPreference('excludedPublishers', {}); + setStoredValue('removedSummaries', {}); + setStoredValue('excludedCategories', {}); + setStoredValue('excludedPublishers', {}); } } /> { - resetPreferences(); + resetStoredValues(); } } onLongPress={ () => { - resetPreferences(true); + resetStoredValues(true); } } /> diff --git a/src/mobile/src/components/walkthroughs/AppearanceWalkthrough.tsx b/src/mobile/src/components/walkthroughs/AppearanceWalkthrough.tsx index 40732d8e9..897cc57e0 100644 --- a/src/mobile/src/components/walkthroughs/AppearanceWalkthrough.tsx +++ b/src/mobile/src/components/walkthroughs/AppearanceWalkthrough.tsx @@ -15,21 +15,21 @@ import { View, Walkthrough, } from '~/components'; -import { Bookmark, SessionContext } from '~/contexts'; +import { SessionContext, TimelineEvent } from '~/contexts'; import { strings } from '~/locales'; export function AppearanceWalkthrough(props: SheetProps) { - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const onDone = React.useCallback(async () => { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps = React.useMemo(() => { return [ @@ -79,7 +79,7 @@ export function AppearanceWalkthrough(props: SheetProps) { hideCard scrollEnabled={ false } initialFormat={ ReadingFormat.Summary } - onFormatChange={ (format) => setPreference('preferredReadingFormat', format) } /> + onFormatChange={ (format) => setStoredValue('preferredReadingFormat', format) } /> ), @@ -113,7 +113,7 @@ export function AppearanceWalkthrough(props: SheetProps) { title: strings.walkthroughs_appearance_selectTheme, }, ]; - }, [onDone, setPreference]); + }, [onDone, setStoredValue]); return ( { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps = React.useMemo(() => [ { diff --git a/src/mobile/src/components/walkthroughs/CustomFeedWalkthrough.tsx b/src/mobile/src/components/walkthroughs/CustomFeedWalkthrough.tsx index 2fa332d5d..8cdcfcfbe 100644 --- a/src/mobile/src/components/walkthroughs/CustomFeedWalkthrough.tsx +++ b/src/mobile/src/components/walkthroughs/CustomFeedWalkthrough.tsx @@ -14,41 +14,41 @@ import { View, Walkthrough, } from '~/components'; -import { Bookmark, SessionContext } from '~/contexts'; +import { SessionContext, TimelineEvent } from '~/contexts'; import { useTheme } from '~/hooks'; import { strings } from '~/locales'; export function CustomFeedWalkthrough(props: SheetProps) { const theme = useTheme(); - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const [categories, setCategories] = React.useState(); const [publishers, setPublishers] = React.useState(); const saveChanges = React.useCallback(async () => { if (categories) { - setPreference( + setStoredValue( 'followedCategories', Object.fromEntries(categories.map((category) => [category, true])) ); } if (publishers) { - setPreference( + setStoredValue( 'followedPublishers', Object.fromEntries(publishers.map((publisher) => [publisher, true])) ); } - }, [categories, publishers, setPreference]); + }, [categories, publishers, setStoredValue]); const onDone = React.useCallback(async () => { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps = React.useMemo(() => [ { diff --git a/src/mobile/src/components/walkthroughs/SentimentWalkthrough.tsx b/src/mobile/src/components/walkthroughs/SentimentWalkthrough.tsx index 5102c2311..33875042e 100644 --- a/src/mobile/src/components/walkthroughs/SentimentWalkthrough.tsx +++ b/src/mobile/src/components/walkthroughs/SentimentWalkthrough.tsx @@ -16,24 +16,24 @@ import { Walkthrough, WalkthroughStep, } from '~/components'; -import { Bookmark, SessionContext } from '~/contexts'; +import { SessionContext, TimelineEvent } from '~/contexts'; import { useInAppBrowser, useTheme } from '~/hooks'; import { strings } from '~/locales'; export function SentimentWalkthrough(props: SheetProps) { - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const { openURL } = useInAppBrowser(); const theme = useTheme(); const onDone = React.useCallback(async () => { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps: WalkthroughStep[] = React.useMemo(() => [ { @@ -233,7 +233,7 @@ export function SentimentWalkthrough(props: SheetProps) { color={ 'white' } haptic onPress={ () => { - setPreference('sentimentEnabled', true); + setStoredValue('sentimentEnabled', true); onDone(); } }> {strings.walkthroughs_sentiment_enable} @@ -243,7 +243,7 @@ export function SentimentWalkthrough(props: SheetProps) { ), title: strings.walkthroughs_sentiment_enableQuestion, }, - ], [theme.colors.link, theme.colors.text, theme.colors.success, openURL, onDone, setPreference]); + ], [theme.colors.link, theme.colors.text, theme.colors.success, openURL, onDone, setStoredValue]); return ( { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps = React.useMemo(() => [ { diff --git a/src/mobile/src/components/walkthroughs/TemplateWalkthrough.tsx b/src/mobile/src/components/walkthroughs/TemplateWalkthrough.tsx index 44c06f1b2..9cdf333b7 100644 --- a/src/mobile/src/components/walkthroughs/TemplateWalkthrough.tsx +++ b/src/mobile/src/components/walkthroughs/TemplateWalkthrough.tsx @@ -10,21 +10,21 @@ import { View, Walkthrough, } from '~/components'; -import { Bookmark, SessionContext } from '~/core'; +import { SessionContext, TimelineEvent } from '~/core'; import { strings } from '~/locales'; export function SharingWalkthrough(props: SheetProps) { - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const onDone = React.useCallback(async () => { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps = React.useMemo(() => [ { diff --git a/src/mobile/src/components/walkthroughs/TriggerWordsWalkthrough.tsx b/src/mobile/src/components/walkthroughs/TriggerWordsWalkthrough.tsx index abbf654d8..7a67405c4 100644 --- a/src/mobile/src/components/walkthroughs/TriggerWordsWalkthrough.tsx +++ b/src/mobile/src/components/walkthroughs/TriggerWordsWalkthrough.tsx @@ -10,21 +10,21 @@ import { View, Walkthrough, } from '~/components'; -import { Bookmark, SessionContext } from '~/core'; +import { SessionContext, TimelineEvent } from '~/core'; import { strings } from '~/locales'; export function TriggerWordsWalkthrough(props: SheetProps) { - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const onDone = React.useCallback(async () => { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps = React.useMemo(() => [ { diff --git a/src/mobile/src/components/walkthroughs/WhatsNewWalkthrough.tsx b/src/mobile/src/components/walkthroughs/WhatsNewWalkthrough.tsx index 0b9fcfec3..f149c7936 100644 --- a/src/mobile/src/components/walkthroughs/WhatsNewWalkthrough.tsx +++ b/src/mobile/src/components/walkthroughs/WhatsNewWalkthrough.tsx @@ -7,21 +7,21 @@ import { Walkthrough, WalkthroughStep, } from '~/components'; -import { Bookmark, SessionContext } from '~/contexts'; +import { SessionContext, TimelineEvent } from '~/contexts'; import { strings } from '~/locales'; export function WhatsNewWalkthrough(props: SheetProps) { - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const onDone = React.useCallback(async () => { - setPreference('viewedFeatures', (prev) => { + setStoredValue('viewedFeatures', (prev) => { const state = { ...prev }; - state[props.sheetId] = new Bookmark(true); + state[props.sheetId] = new TimelineEvent(true); return (prev = state); }); await SheetManager.hide(props.sheetId); - }, [props.sheetId, setPreference]); + }, [props.sheetId, setStoredValue]); const steps: WalkthroughStep[] = React.useMemo(() => [ { diff --git a/src/mobile/src/contexts/notifications/NotificationContext.tsx b/src/mobile/src/contexts/notifications/NotificationContext.tsx index 5fdd806ff..c5793e1cf 100644 --- a/src/mobile/src/contexts/notifications/NotificationContext.tsx +++ b/src/mobile/src/contexts/notifications/NotificationContext.tsx @@ -29,7 +29,7 @@ export function NotificationContextProvider({ children }: React.PropsWithChildre const { fcmToken, enablePush, - setPreference, + setStoredValue, } = React.useContext(SessionContext); const [redirectToSettings, setRedirectToSettings] = React.useState(false); @@ -46,7 +46,7 @@ export function NotificationContextProvider({ children }: React.PropsWithChildre if (!data) { return; } - await setPreference('pushNotifications', (prev) => { + await setStoredValue('pushNotifications', (prev) => { const newState = { ...prev }; for (const [event] of (Object.keys(newState) as SubscriptionEvent[]).entries()) { if (!(event in data)) { @@ -64,13 +64,13 @@ export function NotificationContextProvider({ children }: React.PropsWithChildre } catch (e) { console.log(e); } - }, [fcmToken, getSubscriptionStatus, setPreference]); + }, [fcmToken, getSubscriptionStatus, setStoredValue]); const isRegisteredForRemoteNotifications = React.useCallback(async (redirectOnFail = false) => { const fail = () => { - setPreference('pushNotificationsEnabled', false); - setPreference('fcmToken', undefined); - setPreference('pushNotifications', {}); + setStoredValue('pushNotificationsEnabled', false); + setStoredValue('fcmToken', undefined); + setStoredValue('pushNotifications', {}); if (redirectOnFail) { Linking.openSettings(); } @@ -96,7 +96,7 @@ export function NotificationContextProvider({ children }: React.PropsWithChildre return false; } return true; - }, [setPreference]); + }, [setStoredValue]); const registerRemoteNotifications = React.useCallback((redirectOnFail = false) => { try { @@ -121,8 +121,8 @@ export function NotificationContextProvider({ children }: React.PropsWithChildre return; } const newFcmToken = await messaging().getToken(); - setPreference('pushNotificationsEnabled', true); - setPreference('fcmToken', newFcmToken); + setStoredValue('pushNotificationsEnabled', true); + setStoredValue('fcmToken', newFcmToken); await subscribe({ channel, event: SubscriptionEvent.Default, @@ -184,7 +184,7 @@ export function NotificationContextProvider({ children }: React.PropsWithChildre listeners.forEach(listener => listener.remove()); }; - }, [isRegisteredForRemoteNotifications, redirectToSettings, setPreference, subscribe, syncWithServer]); + }, [isRegisteredForRemoteNotifications, redirectToSettings, setStoredValue, subscribe, syncWithServer]); return ( ; +export type DrawerNavigation = DrawerNavigationProp; +export type StackNavigation = NativeStackNavigationProp; +export type Navigation = DrawerNavigation & StackNavigation; export function useNavigation() { @@ -20,12 +23,12 @@ export function useNavigation() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const navigation = useRNNavigation(); - const { preferredReadingFormat, setPreference } = React.useContext(SessionContext); + const { preferredReadingFormat, setStoredValue } = React.useContext(SessionContext); const navigate = React.useCallback((route: R, params?: RoutingParams[R], stackNav?: Navigation) => { emitEvent('navigate', route); // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (stackNav?.push ?? (navigation as any).push ?? navigation.navigate)(route, params as RoutingParams[R]); + return (stackNav?.push ?? navigation.push ?? navigation.navigate)(route, params as RoutingParams[R]); }, [emitEvent, navigation]); const search = React.useCallback((params: RoutingParams['search'], stackNav?: Navigation) => { @@ -33,9 +36,9 @@ export function useNavigation() { if (!prefilter) { return; } - setPreference('searchHistory', (prev) => Array.from(new Set([prefilter, ...(prev ?? [])])).slice(0, 10)); + setStoredValue('searchHistory', (prev) => Array.from(new Set([prefilter, ...(prev ?? [])])).slice(0, 10)); navigate('search', params, stackNav); - }, [navigate, setPreference]); + }, [navigate, setStoredValue]); const openSummary = React.useCallback((props: RoutingParams['summary'], stackNav?: Navigation) => { navigate('summary', { diff --git a/src/mobile/src/screens/BookmarksScreen.tsx b/src/mobile/src/screens/BookmarksScreen.tsx index e470839a4..badb3e60f 100644 --- a/src/mobile/src/screens/BookmarksScreen.tsx +++ b/src/mobile/src/screens/BookmarksScreen.tsx @@ -32,7 +32,7 @@ export function BookmarksScreen({ navigation }: ScreenComponent<'bookmarks'>) { bookmarkCount, readSummaries, preferredReadingFormat, - setPreference, + setStoredValue, viewFeature, } = React.useContext(SessionContext); const { interactWithSummary } = useApiClient(); @@ -95,7 +95,7 @@ export function BookmarksScreen({ navigation }: ScreenComponent<'bookmarks'>) { contained beveled p={ 6 } - onPress={ () => setPreference('bookmarkedSummaries', (prev) => { + onPress={ () => setStoredValue('bookmarkedSummaries', (prev) => { const state = { ...prev }; for (const [id] of Object.entries(state)) { if (id in (readSummaries ?? {})) { diff --git a/src/mobile/src/screens/StatsScreen.tsx b/src/mobile/src/screens/StatsScreen.tsx index 96016bce7..7aef503f5 100644 --- a/src/mobile/src/screens/StatsScreen.tsx +++ b/src/mobile/src/screens/StatsScreen.tsx @@ -15,7 +15,7 @@ export function StatsScreen({ route: _route, navigation: _navigation, }: ScreenComponent<'stats'>) { - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); return ( @@ -25,7 +25,7 @@ export function StatsScreen({ diff --git a/src/mobile/src/screens/settings/CategoryPickerScreen.tsx b/src/mobile/src/screens/settings/CategoryPickerScreen.tsx index 7f41c2462..6536cf5e1 100644 --- a/src/mobile/src/screens/settings/CategoryPickerScreen.tsx +++ b/src/mobile/src/screens/settings/CategoryPickerScreen.tsx @@ -9,16 +9,16 @@ import { useNavigation } from '~/hooks'; export function CategoryPickerScreen() { const { navigation } = useNavigation(); - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const pickerRef = React.useRef<{ value: string[] }>(null); useFocusEffect(React.useCallback(() => { navigation?.addListener('beforeRemove', async () => { const { value = [] } = pickerRef.current ?? {}; - setPreference('followedCategories', (prev) => { + setStoredValue('followedCategories', (prev) => { const removed = Object.keys({ ...prev }).filter((publisher) => !value.includes(publisher)) ?? []; - setPreference('favoritedCategories', (favorited) => { + setStoredValue('favoritedCategories', (favorited) => { const state = { ...favorited }; for (const publisher of removed) { delete state[publisher]; @@ -28,7 +28,7 @@ export function CategoryPickerScreen() { return (prev = Object.fromEntries(value.map((publisher) => [publisher, true]) ?? [])); }); }); - }, [pickerRef, navigation, setPreference])); + }, [pickerRef, navigation, setStoredValue])); return ( diff --git a/src/mobile/src/screens/settings/PublisherPickerScreen.tsx b/src/mobile/src/screens/settings/PublisherPickerScreen.tsx index c68fae732..6c56cf028 100644 --- a/src/mobile/src/screens/settings/PublisherPickerScreen.tsx +++ b/src/mobile/src/screens/settings/PublisherPickerScreen.tsx @@ -9,16 +9,16 @@ import { useNavigation } from '~/hooks'; export function PublisherPickerScreen() { const { navigation } = useNavigation(); - const { setPreference } = React.useContext(SessionContext); + const { setStoredValue } = React.useContext(SessionContext); const pickerRef = React.useRef<{ value: string[] }>(null); useFocusEffect(React.useCallback(() => { navigation?.addListener('beforeRemove', async () => { const { value = [] } = pickerRef.current ?? {}; - setPreference('followedPublishers', (prev) => { + setStoredValue('followedPublishers', (prev) => { const removed = Object.keys({ ...prev }).filter((publisher) => !value.includes(publisher)) ?? []; - setPreference('favoritedPublishers', (favorited) => { + setStoredValue('favoritedPublishers', (favorited) => { const state = { ...favorited }; for (const publisher of removed) { delete state[publisher]; @@ -28,7 +28,7 @@ export function PublisherPickerScreen() { return (prev = Object.fromEntries(value.map((publisher) => [publisher, true]) ?? [])); }); }); - }, [pickerRef, navigation, setPreference])); + }, [pickerRef, navigation, setStoredValue])); return ( diff --git a/src/mobile/src/screens/settings/ReadingFormatPickerScreen.tsx b/src/mobile/src/screens/settings/ReadingFormatPickerScreen.tsx index b1b8e1a90..6ad83d5b3 100644 --- a/src/mobile/src/screens/settings/ReadingFormatPickerScreen.tsx +++ b/src/mobile/src/screens/settings/ReadingFormatPickerScreen.tsx @@ -9,7 +9,7 @@ import { import { SessionContext } from '~/contexts'; export function ReadingFormatPickerScreen() { - const { preferredReadingFormat, setPreference } = React.useContext(SessionContext); + const { preferredReadingFormat, setStoredValue } = React.useContext(SessionContext); return ( @@ -17,7 +17,7 @@ export function ReadingFormatPickerScreen() { sample hideCard initialFormat={ preferredReadingFormat ?? ReadingFormat.Bullets } - onFormatChange={ (format) => setPreference('preferredReadingFormat', format) } /> + onFormatChange={ (format) => setStoredValue('preferredReadingFormat', format) } /> ); diff --git a/src/mobile/src/utils/usePlatformTools.ts b/src/mobile/src/utils/usePlatformTools.ts index 040e3a256..25b43d615 100644 --- a/src/mobile/src/utils/usePlatformTools.ts +++ b/src/mobile/src/utils/usePlatformTools.ts @@ -18,7 +18,7 @@ import * as Localization from '~/locales'; export function usePlatformTools() { - const { latestVersion = '', setPreference } = React.useContext(SessionContext); + const { latestVersion = '', setStoredValue } = React.useContext(SessionContext); const [screenReaderEnabled, setScreenReaderEnabled] = React.useState(false); const getUserAgent = () => { @@ -34,7 +34,7 @@ export function usePlatformTools() { const emitEvent = React.useCallback(async (event: E, mutation?: PreferenceMutation, state?: PreferenceState) => { const version = latestVersion || await VersionCheck.getLatestVersion(); if (!latestVersion) { - setPreference('latestVersion', version); + setStoredValue('latestVersion', version); } if (!__DEV__ && VersionCheck.getCurrentVersion() <= version) { analytics().logEvent(event.replace(/-/g, '_'), { @@ -46,7 +46,7 @@ export function usePlatformTools() { }); } DeviceEventEmitter.emit(event, mutation, state); - }, [latestVersion, setPreference]); + }, [latestVersion, setStoredValue]); React.useEffect(() => { const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setScreenReaderEnabled);