Skip to content

Commit

Permalink
Merge pull request #867 from NoodleOfDeath/dev
Browse files Browse the repository at this point in the history
~api
  • Loading branch information
NoodleOfDeath authored Jan 29, 2024
2 parents 31dde96 + 0ab2341 commit 6b34dd7
Show file tree
Hide file tree
Showing 32 changed files with 439 additions and 350 deletions.
143 changes: 101 additions & 42 deletions src/core/src/client/contexts/storage/StorageContext.tsx

Large diffs are not rendered by default.

61 changes: 15 additions & 46 deletions src/core/src/client/contexts/storage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export type StorageState<E extends StorageEventName> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any;

export const SYNCABLE_SETTINGS: (keyof Storage)[] = [
export const SYNCABLE_PREFERENCES: (keyof Storage)[] = [
'colorScheme',
'compactSummaries',
'showShortSummary',
Expand All @@ -224,6 +224,12 @@ export const SYNCABLE_SETTINGS: (keyof Storage)[] = [
'lineHeightMultiplier',
'pushNotifications',
'pushNotificationsEnabled',
'viewedFeatures',
'searchHistory',
'readNotifications',
];

export const SYNCABLE_METRICS: (keyof Storage)[] = [
'bookmarkedSummaries',
'readSummaries',
'removedSummaries',
Expand All @@ -233,57 +239,20 @@ export const SYNCABLE_SETTINGS: (keyof Storage)[] = [
'followedCategories',
'favoritedCategories',
'excludedCategories',
'viewedFeatures',
'searchHistory',
'readNotifications',
];

export type SyncableSetting = typeof SYNCABLE_SETTINGS[number];
export const ALL_SYNCABLE: (keyof Storage)[] = [
...SYNCABLE_PREFERENCES,
...SYNCABLE_METRICS,
];

export type SyncableSetting = typeof SYNCABLE_PREFERENCES[number];

export const SYNCABLE_IO_IN_DEFAULT = <K extends SyncableSetting>(value?: object) => value as Storage[K];
export const SYNCABLE_IO_OUT_DEFAULT = <K extends SyncableSetting>(value?: Storage[K]) => JSON.stringify(value);

export const SYNCABLE_IO_IN_BOOLEAN_MAP = <K extends SyncableSetting>(value?: object): Storage[K] => {
if (value) {
if (Array.isArray(value)) {
return value.reduce((acc, key) => ({ ...acc, [key]: true }), {}) as Storage[K];
}
}
return {} as Storage[K];
};

export const SYNCABLE_IO_OUT_DATED_MAP = <K extends SyncableSetting>(value?: Storage[K]) => {
if (value) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const remap = Object.fromEntries(Object.keys(value || {}).map((key) => [key, (value as any)[key].createdAt ?? new Date()]));
return JSON.stringify(remap);
}
return '{}';
};

export const SYNCABLE_IO_IN: { [K in SyncableSetting]?: ((value?: object) => Storage[K]) } = {
excludedCategories: SYNCABLE_IO_IN_BOOLEAN_MAP<'excludedCategories'>,
excludedPublishers: SYNCABLE_IO_IN_BOOLEAN_MAP<'excludedPublishers'>,
favoritedCategories: SYNCABLE_IO_IN_BOOLEAN_MAP<'favoritedCategories'>,
favoritedPublishers: SYNCABLE_IO_IN_BOOLEAN_MAP<'favoritedPublishers'>,
followedCategories: SYNCABLE_IO_IN_BOOLEAN_MAP<'followedCategories'>,
followedPublishers: SYNCABLE_IO_IN_BOOLEAN_MAP<'followedPublishers'>,
removedSummaries: SYNCABLE_IO_IN_BOOLEAN_MAP<'removedSummaries'>,
};

export const SYNCABLE_IO_OUT: { [K in SyncableSetting]?: ((value?: Storage[K]) => string) } = {
bookmarkedSummaries: SYNCABLE_IO_OUT_DATED_MAP,
excludedCategories: (value) => JSON.stringify(Object.keys(value || {})),
excludedPublishers: (value) => JSON.stringify(Object.keys(value || {})),
favoritedCategories: (value) => JSON.stringify(Object.keys(value || {})),
favoritedPublishers: (value) => JSON.stringify(Object.keys(value || {})),
followedCategories: (value) => JSON.stringify(Object.keys(value || {})),
followedPublishers: (value) => JSON.stringify(Object.keys(value || {})),
removedSummaries: (value) => JSON.stringify(Object.keys(value || {})),
};

export const SyncableIoIn = <K extends SyncableSetting>(key?: K) => key ? SYNCABLE_IO_IN[key] || SYNCABLE_IO_IN_DEFAULT : SYNCABLE_IO_IN_DEFAULT;
export const SyncableIoOut = <K extends SyncableSetting>(key?: K) => key ? SYNCABLE_IO_OUT[key] || SYNCABLE_IO_OUT_DEFAULT : SYNCABLE_IO_OUT_DEFAULT;
export const SyncableIoIn = SYNCABLE_IO_IN_DEFAULT;
export const SyncableIoOut = SYNCABLE_IO_OUT_DEFAULT;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Methods = {
Expand Down
6 changes: 3 additions & 3 deletions src/mobile/src/components/post/summary/SummaryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function SummaryList({
}: SummaryListProps) {

// hooks
const { navigation } = useNavigation();
const { navigate, navigation } = useNavigation();
const theme = useTheme();

// contexts
Expand Down Expand Up @@ -213,14 +213,14 @@ export function SummaryList({
} else if (onFormatChange) {
onFormatChange(summary, format ?? preferredReadingFormat ?? ReadingFormat.Bullets);
} else {
navigation?.push('summary', {
navigate('summary', {
initialFormat: format ?? preferredReadingFormat ?? ReadingFormat.Bullets,
keywords: parseKeywords(filter),
summary,
});
}
},
[interactWithSummary, isTablet, onFormatChange, preferredReadingFormat, navigation, filter]
[interactWithSummary, isTablet, onFormatChange, preferredReadingFormat, navigate, filter]
);

useFocusEffect(React.useCallback(() => {
Expand Down
46 changes: 27 additions & 19 deletions src/mobile/src/hooks/useNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';

import { DrawerNavigationProp } from '@react-navigation/drawer';
import { useNavigation as useRNNavigation } from '@react-navigation/native';
import { StackActions } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';

import { useInAppBrowser } from './useInAppBrowser';
Expand All @@ -13,6 +14,7 @@ import {
ReadingFormat,
} from '~/api';
import { StorageContext } from '~/contexts';
import { HOME_STACK_KEYS, LOGIN_STACK_KEYS } from '~/navigation';
import { NavigationID, RoutingParams } from '~/screens';
import { readingFormat, usePlatformTools } from '~/utils';

Expand All @@ -33,42 +35,48 @@ export function useNavigation() {
setStoredValue,
} = React.useContext(StorageContext);

const navigate = React.useCallback(<R extends keyof RoutingParams>(route: R, params?: RoutingParams[R], stackNav?: Navigation) => {
const navigate = React.useCallback(<R extends keyof RoutingParams>(route: R, params?: RoutingParams[R]) => {
emitStorageEvent('navigate', route);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (stackNav?.push ?? navigation.push ?? navigation.navigate)(route, params as RoutingParams[R]);
if (LOGIN_STACK_KEYS.includes(route)) {
navigation.navigate('login');
return navigation.dispatch(StackActions.push(route, params));
} else
if (HOME_STACK_KEYS.includes(route)) {
navigation.navigate('home');
return navigation.dispatch(StackActions.push(route, params));
}
}, [emitStorageEvent, navigation]);

const beginSearch = React.useCallback((params: RoutingParams['search'], stackNav?: Navigation) => {
navigate('search', params, stackNav);
const beginSearch = React.useCallback((params: RoutingParams['search']) => {
navigate('search', params);
}, [navigate]);

const search = React.useCallback((params: RoutingParams['summaryList'], stackNav?: Navigation) => {
const search = React.useCallback((params: RoutingParams['summaryList']) => {
const prefilter = params.prefilter;
if (!prefilter) {
return;
}
setStoredValue('searchHistory', (prev) => Array.from(new Set([prefilter, ...(prev ?? [])])).slice(0, 100));
navigate('summaryList', params, stackNav);
navigate('summaryList', params);
}, [navigate, setStoredValue]);

const openSummary = React.useCallback((props: RoutingParams['summary'], stackNav?: Navigation) => {
const openSummary = React.useCallback((props: RoutingParams['summary']) => {
interactWithSummary(typeof props.summary === 'number' ? props.summary : props.summary.id, InteractionType.Read, { metadata: { format: props.initialFormat ?? preferredReadingFormat ?? ReadingFormat.Bullets } });
navigate('summary', {
...props,
initialFormat: props.initialFormat ?? preferredReadingFormat ?? ReadingFormat.Bullets,
}, stackNav);
});
}, [navigate, preferredReadingFormat, interactWithSummary]);

const openPublisher = React.useCallback((publisher: PublicPublisherAttributes, stackNav?: Navigation) => {
navigate('publisher', { publisher }, stackNav);
const openPublisher = React.useCallback((publisher: PublicPublisherAttributes) => {
navigate('publisher', { publisher });
}, [navigate]);

const openCategory = React.useCallback((category: PublicCategoryAttributes, stackNav?: Navigation) => {
navigate('category', { category }, stackNav);
const openCategory = React.useCallback((category: PublicCategoryAttributes) => {
navigate('category', { category });
}, [navigate]);

const router = React.useCallback(({ url, stackNav }: { url: string, stackNav?: Navigation }) => {
const router = React.useCallback(({ url }: { url: string }) => {
// http://localhost:6969/read/?s=158&f=casual
// https://dev.readless.ai/read/?s=158&f=casual
// https://www.readless.ai/read/?s=4070&f=bullets
Expand All @@ -85,7 +93,7 @@ export function useNavigation() {
});
}
if (route === 'verify') {
navigate('verifyOtp', { code: params['code'], otp: params['otp'] }, stackNav);
navigate('verifyOtp', { code: params['code'], otp: params['otp'] });
} else
if (route === 'delete') {
openURL(url);
Expand All @@ -96,7 +104,7 @@ export function useNavigation() {
return;
}
const initialFormat = readingFormat(params['f']);
openSummary({ initialFormat, summary }, stackNav);
openSummary({ initialFormat, summary });
} else
if (route === 'top') {
navigate('topStories');
Expand All @@ -109,14 +117,14 @@ export function useNavigation() {
if (!filter) {
return;
}
search({ prefilter: filter }, stackNav);
search({ prefilter: filter });
} else
if (route === 'publisher') {
const publisher = params['publisher']?.trim();
if (!publisher) {
return;
}
openPublisher({ displayName: '', name: publisher }, stackNav);
openPublisher({ displayName: '', name: publisher });
} else
if (route === 'category') {
const category = params['category']?.trim();
Expand All @@ -125,7 +133,7 @@ export function useNavigation() {
}
openCategory({
displayName: '', icon: '', name: category,
}, stackNav);
});
}
}, [navigate, openURL, openSummary, search, openPublisher, openCategory]);

Expand Down
66 changes: 52 additions & 14 deletions src/mobile/src/navigation/RootNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,32 @@ import {
} from 'react-native';

import { APP_STORE_LINK, PLAY_STORE_LINK } from '@env';
import { NavigationContainer } from '@react-navigation/native';
import { BottomTabNavigationOptions } from '@react-navigation/bottom-tabs';
import {
EventMapBase,
NavigationContainer,
NavigationState,
RouteConfig,
} from '@react-navigation/native';
import ms from 'ms';
import { SheetProvider } from 'react-native-actions-sheet';
import InAppReview from 'react-native-in-app-review';

import { TabNavigator } from './TabNavigator';
import { RoutedScreen } from './RoutedScreen';
import { StackNavigator } from './StackNavigator';
import { HOME_STACK, LOGIN_STACK } from './stacks';
import { TabbedNavigator } from './TabbedNavigator';
import {
ACCOUNT_STACK,
HOME_STACK,
LOGIN_STACK,
SETTINGS_STACK,
} from './stacks';

import {
ActivityIndicator,
Button,
Dialog,
Icon,
MediaPlayer,
Screen,
Text,
Expand All @@ -35,12 +47,12 @@ import {
} from '~/contexts';
import { useAppState, useTheme } from '~/hooks';
import { strings } from '~/locales';
import { NAVIGATION_LINKING_OPTIONS } from '~/screens';
import { NAVIGATION_LINKING_OPTIONS, RoutingParams } from '~/screens';
import { usePlatformTools } from '~/utils';

export function HomeTab() {
return (
<RoutedScreen navigationID='HomeTabNav' safeArea={ false }>
<RoutedScreen safeArea={ false }>
<StackNavigator
id='HomeTabNav'
initialRouteName='home'
Expand All @@ -49,11 +61,39 @@ export function HomeTab() {
);
}

const ROOT_TABS = [
export function SettingsTab() {
return (
<RoutedScreen safeArea={ false }>
<StackNavigator
id='SettingsTabNav'
initialRouteName='settings'
screens={ [...SETTINGS_STACK, ...ACCOUNT_STACK] } />
</RoutedScreen>
);
}

const ROOT_TABS: RouteConfig<
RoutingParams,
keyof RoutingParams,
NavigationState,
BottomTabNavigationOptions,
EventMapBase
>[] = [
{
component: HomeTab,
tabBarIcon: 'home',
title: strings.home,
name: 'homeTab',
options: {
tabBarIcon: () => <Icon name='home' />,
tabBarLabel: strings.home,
},
},
{
component: SettingsTab,
name: 'settingsTab',
options: {
tabBarIcon: () => <Icon name='cog' />,
tabBarLabel: strings.settings,
},
},
];

Expand Down Expand Up @@ -224,16 +264,14 @@ export function RootNavigator() {
<SheetProvider>
{(userData?.valid || userData?.unlinked) ? (
<React.Fragment>
<TabNavigator
<TabbedNavigator
id="rootTabNav"
screens={ ROOT_TABS }
screenOptions={ {
headerShown: false,
} }
/>
screenOptions={ { headerShown: false } } />
<MediaPlayer visible={ Boolean(currentTrack) } />
</React.Fragment>
) : (
<RoutedScreen safeArea={ false } navigationID='loginStackNav'>
<RoutedScreen safeArea={ false }>
<StackNavigator
id="loginStackNav"
screens={ LOGIN_STACK }
Expand Down
15 changes: 6 additions & 9 deletions src/mobile/src/navigation/RoutedScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,28 @@ import { useFocusEffect } from '@react-navigation/native';
import { Screen, ScreenProps } from '~/components';
import { StorageContext } from '~/contexts';
import { useNavigation } from '~/hooks';
import { NavigationID } from '~/screens';

export type RoutedScreenProps = ScreenProps & {
navigationID: NavigationID;
};
export type RoutedScreenProps = ScreenProps;

export function RoutedScreen({ navigationID, ...props }: RoutedScreenProps) {
export function RoutedScreen({ ...props }: RoutedScreenProps) {

const { navigation, router } = useNavigation();
const { router } = useNavigation();
const { loadedInitialUrl, setLoadedInitialUrl } = React.useContext(StorageContext);

useFocusEffect(React.useCallback(() => {
const subscriber = Linking.addEventListener('url', ({ url }) => {
router({ stackNav: navigation?.getParent(navigationID), url });
router({ url });
});
if (!loadedInitialUrl) {
Linking.getInitialURL().then((url) => {
if (url) {
setLoadedInitialUrl(true);
router({ stackNav: navigation?.getParent(navigationID), url });
router({ url });
}
});
}
return () => subscriber.remove();
}, [router, loadedInitialUrl, setLoadedInitialUrl, navigation, navigationID]));
}, [router, loadedInitialUrl, setLoadedInitialUrl]));

return (
<Screen { ...props } />
Expand Down
Loading

0 comments on commit 6b34dd7

Please sign in to comment.