Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/update-expo-to-51 #539

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ff637a0
Install zustand
enigma0Z Dec 10, 2024
aeca337
Convert root store to typescript & zustand
enigma0Z Dec 10, 2024
f9e09bd
Reference new zustand root store
enigma0Z Dec 10, 2024
b362a19
Fix test to work with hooks
enigma0Z Dec 10, 2024
90850f2
Add react testing library & jest-environment-jsdom
enigma0Z Dec 10, 2024
6c1891f
Make root store test more concise
enigma0Z Dec 10, 2024
5da37f1
Port DownloadStore/DownloadModel to zustand
enigma0Z Dec 10, 2024
89c8e64
Remove commented-out code
enigma0Z Dec 10, 2024
da8f22c
Remove commented-out code
enigma0Z Dec 10, 2024
6ab000e
Remove commented-out code
enigma0Z Dec 10, 2024
f9b41d1
Convert ServerModel and ServerStore to zustand
enigma0Z Dec 10, 2024
8f2ff18
Remove commented-out code
enigma0Z Dec 10, 2024
17e18fb
Fix broken tests/refs with refactor of mediastore
enigma0Z Dec 10, 2024
a4bc5ed
Convert setting store to TS
enigma0Z Dec 10, 2024
73c4790
Convert settingstore to use zustand
enigma0Z Dec 10, 2024
889dd5e
Remove usage of mobx observer
enigma0Z Dec 10, 2024
9a1c05a
Remove mobx action implementations
enigma0Z Dec 11, 2024
0e18e16
Remove mobx action implementations
enigma0Z Dec 11, 2024
bc387f4
Remove mobx action implementations
enigma0Z Dec 11, 2024
da56ce7
Add generic setter function for the stores that need it, convert all …
enigma0Z Dec 11, 2024
5cb35f0
Conver setting store to use setters
enigma0Z Dec 11, 2024
0ccf45c
Update rootStore setters
enigma0Z Dec 11, 2024
1e5bbea
Update mediastore to use setters
enigma0Z Dec 11, 2024
e8bd175
Reset all stores correctly since rootStore.reset() was refactored
enigma0Z Dec 11, 2024
b2a88e4
Fix render loop with using a set state on app initialization
enigma0Z Dec 11, 2024
163010e
Persist zustand storage, migrate mobx stores
enigma0Z Dec 11, 2024
23722c2
Fix refresh web view (which I broke trying to get tests working)
enigma0Z Dec 11, 2024
2efecc2
Remove usage of async trunk
enigma0Z Dec 11, 2024
00a4abf
Restore docstrings on root store
enigma0Z Dec 11, 2024
bf86687
Fix serializer tests
enigma0Z Dec 11, 2024
7ec4391
Fix warnings
enigma0Z Dec 11, 2024
d7825a6
Update tests to use setters
enigma0Z Dec 11, 2024
c0b1533
Merge remote-tracking branch 'upstream/master' into feature/migrate-m…
enigma0Z Dec 11, 2024
c8184ce
Remove mobx packages
enigma0Z Dec 11, 2024
32fad17
Linter fixups
enigma0Z Dec 11, 2024
8d8ed65
Clear eslint whitespace errors
enigma0Z Dec 11, 2024
789b378
Ignore eslint import error (I suspect an outdated version of react na…
enigma0Z Dec 11, 2024
209a2ac
Tag the silly eslint import error with the issue it's tracked under.
enigma0Z Dec 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 83 additions & 20 deletions App.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import * as Font from 'expo-font';
import * as ScreenOrientation from 'expo-screen-orientation';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { observer } from 'mobx-react-lite';
import { AsyncTrunk } from 'mobx-sync-lite';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useState } from 'react';
import { Alert, useColorScheme } from 'react-native';
Expand All @@ -27,30 +25,94 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';

import ThemeSwitcher from './components/ThemeSwitcher';
import { useStores } from './hooks/useStores';
import DownloadModel from './models/DownloadModel';
import ServerModel from './models/ServerModel';
import RootNavigator from './navigation/RootNavigator';
import { ensurePathExists } from './utils/File';
import StaticScriptLoader from './utils/StaticScriptLoader';

// Import i18n configuration
import './i18n';

const App = observer(({ skipLoadingScreen }) => {
const App = ({ skipLoadingScreen }) => {
const [ isSplashReady, setIsSplashReady ] = useState(false);
const { rootStore } = useStores();
const { rootStore, downloadStore, settingStore, mediaStore, serverStore } = useStores();
const { theme } = useContext(ThemeContext);

rootStore.settingStore.systemThemeId = useColorScheme();
// Using a hook here causes a render loop; what is the point of this setting?
// settingStore.set({systemThemeId: useColorScheme()});
settingStore.systemThemeId = useColorScheme();

SplashScreen.preventAutoHideAsync();

const trunk = new AsyncTrunk(rootStore, {
storage: AsyncStorage
});

const hydrateStores = async () => {
await trunk.init();
// TODO: In release n+2 from this point, remove this conversion code.
const mobx_store_value = await AsyncStorage.getItem('__mobx_sync__'); // Store will be null if it's not set

if (mobx_store_value !== null) {
console.info('Migrating mobx store to zustand');
const mobx_store = JSON.parse(mobx_store_value);

// Root Store
for (const key of Object.keys(mobx_store).filter(k => k.search('Store') === -1)) {
rootStore.set({ key: mobx_store[key] });
}

// MediaStore
for (const key of Object.keys(mobx_store.mediaStore)) {
mediaStore.set({ key: mobx_store.mediaStore[key] });
}

/**
* Server store & download store need some special treatment because they
* are not simple key-value pair stores. Each contains one key which is a
* list of Model objects that represent the contents of their respective
* stores.
*
* zustand requires a custom storage engine for these for proper
* serialization and deserialization (written in each storage's module),
* but this code is needed to get them over the hump from mobx to zustand.
*/
// DownloadStore
const mobxDownloads = mobx_store.downloadStore.downloads;
const migratedDownloads = new Map();
if (Object.keys(mobxDownloads).length > 0) {
for (const [ key, value ] of Object.getEntries(mobxDownloads)) {
migratedDownloads.set(key, new DownloadModel(
value.itemId,
value.serverId,
value.serverUrl,
value.apiKey,
value.title,
value.fileName,
value.downloadUrl
));
}
}
downloadStore.set({ downloads: migratedDownloads });

rootStore.storeLoaded = true;
// ServerStore
const mobxServers = mobx_store.serverStore.servers;
const migratedServers = [];
if (Object.keys(mobxServers).length > 0) {
for (const item of mobxServers) {
migratedServers.push(new ServerModel(item.id, new URL(item.url), item.info));
}
}
serverStore.set({ servers: migratedServers });

// SettingStore
for (const key of Object.keys(mobx_store.settingStore)) {
console.info('SettingStore', key);
settingStore.set({ key: mobx_store.settingStore[key] });
}

// TODO: Confirm zustand has objects in async storage
// TODO: Remove mobx sync item from async storage
// AsyncStorage.removeItem('__mobx_sync__')
}

rootStore.set({ storeLoaded: true });
};

const loadImages = () => {
Expand Down Expand Up @@ -79,6 +141,7 @@ const App = observer(({ skipLoadingScreen }) => {
};

useEffect(() => {
// Set base app theme
// Hydrate mobx data stores
hydrateStores();

Expand All @@ -87,16 +150,16 @@ const App = observer(({ skipLoadingScreen }) => {
}, []);

useEffect(() => {
console.info('rotation lock setting changed!', rootStore.settingStore.isRotationLockEnabled);
if (rootStore.settingStore.isRotationLockEnabled) {
console.info('rotation lock setting changed!', settingStore.isRotationLockEnabled);
if (settingStore.isRotationLockEnabled) {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
} else {
ScreenOrientation.unlockAsync();
}
}, [ rootStore.settingStore.isRotationLockEnabled ]);
}, [ settingStore.isRotationLockEnabled ]);

const updateScreenOrientation = async () => {
if (rootStore.settingStore.isRotationLockEnabled) {
if (settingStore.isRotationLockEnabled) {
if (rootStore.isFullscreen) {
// Lock to landscape orientation
// For some reason video apps on iPhone use LANDSCAPE_RIGHT ¯\_(ツ)_/¯
Expand Down Expand Up @@ -163,34 +226,34 @@ const App = observer(({ skipLoadingScreen }) => {
});
};

rootStore.downloadStore.downloads
downloadStore.downloads
.forEach(download => {
if (!download.isComplete && !download.isDownloading) {
downloadFile(download);
}
});
}, [ rootStore.deviceId, rootStore.downloadStore.downloads.size ]);
}, [ rootStore.deviceId, downloadStore.downloads.size ]);

if (!(isSplashReady && rootStore.storeLoaded) && !skipLoadingScreen) {
return null;
}

return (
<SafeAreaProvider>
<ThemeProvider theme={rootStore.settingStore.theme.Elements}>
<ThemeProvider theme={settingStore.getTheme().Elements}>
<ThemeSwitcher />
<StatusBar
style='light'
backgroundColor={theme.colors.grey0}
hidden={rootStore.isFullscreen}
/>
<NavigationContainer theme={rootStore.settingStore.theme.Navigation}>
<NavigationContainer theme={settingStore.getTheme().Navigation}>
<RootNavigator />
</NavigationContainer>
</ThemeProvider>
</SafeAreaProvider>
);
});
};

App.propTypes = {
skipLoadingScreen: PropTypes.bool
Expand Down
65 changes: 65 additions & 0 deletions __mocks__/zustand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// __mocks__/zustand.ts
import { act } from '@testing-library/react';
import type * as ZustandExportedTypes from 'zustand';
export * from 'zustand';

const { create: actualCreate, createStore: actualCreateStore } =
jest.requireActual<typeof ZustandExportedTypes>('zustand');

Check failure on line 7 in __mocks__/zustand.ts

View workflow job for this annotation

GitHub Actions / TypeScript Build

Cannot find name 'jest'.

Check failure on line 7 in __mocks__/zustand.ts

View workflow job for this annotation

GitHub Actions / TypeScript Build

Cannot find name 'jest'.

// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>();

const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>
) => {
const store = actualCreate(stateCreator);
const initialState = store.getInitialState();
storeResetFns.add(() => {
store.setState(initialState, true);
});
return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>
) => {
console.log('zustand create mock');

// to support curried version of create
return typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried;
}) as typeof ZustandExportedTypes.create;

const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>
) => {
const store = actualCreateStore(stateCreator);
const initialState = store.getInitialState();
storeResetFns.add(() => {
store.setState(initialState, true);
});
return store;
};

// when creating a store, we get its initial state, create a reset function and add it in the set
export const createStore = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>
) => {
console.log('zustand createStore mock');

// to support curried version of createStore
return typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried;
}) as typeof ZustandExportedTypes.createStore;

// reset all stores after each test run
afterEach(() => {

Check failure on line 59 in __mocks__/zustand.ts

View workflow job for this annotation

GitHub Actions / TypeScript Build

Cannot find name 'afterEach'.

Check failure on line 59 in __mocks__/zustand.ts

View workflow job for this annotation

GitHub Actions / TypeScript Build

Cannot find name 'afterEach'.
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn();
});
});
});
39 changes: 20 additions & 19 deletions components/AudioPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
*/

import { Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av';
import { observer } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';

import MediaTypes from '../constants/MediaTypes';
import { useStores } from '../hooks/useStores';
import { msToTicks } from '../utils/Time';

const AudioPlayer = observer(() => {
const { rootStore } = useStores();
const AudioPlayer = () => {
const { mediaStore } = useStores();

const [ player, setPlayer ] = useState();

Expand Down Expand Up @@ -57,48 +56,50 @@ const AudioPlayer = observer(() => {
didJustFinish === undefined ||
isPlaying === undefined ||
positionMs === undefined ||
rootStore.mediaStore.isFinished
mediaStore.isFinished
) {
return;
}
rootStore.mediaStore.isFinished = didJustFinish;
rootStore.mediaStore.isPlaying = isPlaying;
rootStore.mediaStore.positionTicks = msToTicks(positionMs);
mediaStore.set({
isFinished: didJustFinish,
isPlaying: isPlaying,
positionTicks: msToTicks(positionMs)
});
});
setPlayer(sound);
}
};

if (rootStore.mediaStore.type === MediaTypes.Audio) {
if (mediaStore.type === MediaTypes.Audio) {
createPlayer({
uri: rootStore.mediaStore.uri,
positionMillis: rootStore.mediaStore.positionMillis
uri: mediaStore.uri,
positionMillis: mediaStore.positionMillis
});
}
}, [ rootStore.mediaStore.type, rootStore.mediaStore.uri ]);
}, [ mediaStore.type, mediaStore.uri ]);

// Update the play/pause state when the store indicates it should
useEffect(() => {
if (rootStore.mediaStore.type === MediaTypes.Audio && rootStore.mediaStore.shouldPlayPause) {
if (rootStore.mediaStore.isPlaying) {
if (mediaStore.type === MediaTypes.Audio && mediaStore.shouldPlayPause) {
if (mediaStore.isPlaying) {
player?.pauseAsync();
} else {
player?.playAsync();
}
rootStore.mediaStore.shouldPlayPause = false;
mediaStore.set({ shouldPlayPause: false });
}
}, [ rootStore.mediaStore.shouldPlayPause ]);
}, [ mediaStore.shouldPlayPause ]);

// Stop the player when the store indicates it should stop playback
useEffect(() => {
if (rootStore.mediaStore.type === MediaTypes.Audio && rootStore.mediaStore.shouldStop) {
if (mediaStore.type === MediaTypes.Audio && mediaStore.shouldStop) {
player?.stopAsync();
player?.unloadAsync();
rootStore.mediaStore.shouldStop = false;
mediaStore.set({ shouldStop: false });
}
}, [ rootStore.mediaStore.shouldStop ]);
}, [ mediaStore.shouldStop ]);

return <></>;
});
};

export default AudioPlayer;
Loading
Loading