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

Upgrade to Expo 52 #584

Draft
wants to merge 64 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 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
809f564
Migrate masked view to new package
enigma0Z Dec 12, 2024
31a74f3
Dependency hell (react, react-native, async-storage, types for react)
enigma0Z Dec 12, 2024
895e41b
Update expo
enigma0Z Dec 12, 2024
def83b4
Fix font issues in tests
enigma0Z Dec 12, 2024
3ccf602
Update podfile & IOS deployment parts and bits
enigma0Z Dec 12, 2024
c55a5f8
Bump to 1.7.0
enigma0Z Dec 12, 2024
ba2595d
This prop was removed from latest react-native
enigma0Z Dec 12, 2024
4d3be76
Downgrade react-native-webview to fix `RNCWebViewModule could not be …
enigma0Z Dec 12, 2024
c52a9f2
Clear fowardRef warning
enigma0Z Dec 12, 2024
9402486
Update snapshots
enigma0Z Dec 13, 2024
275cf46
Fix main test suite and update snapshots
enigma0Z Dec 13, 2024
57692cb
Update downstream testing deps
enigma0Z Dec 13, 2024
20ba35f
Revert "Downgrade react-native-webview to fix `RNCWebViewModule could…
enigma0Z Dec 13, 2024
be6bdbe
Update to new ion icon names (they removed the platform prefixes)
enigma0Z Dec 13, 2024
82072b7
Move WebView (NativeShell, Refresh) tests out of the way, there is a …
enigma0Z Dec 13, 2024
2623f51
Update forwardRef usage to reflect corrent pattern for using propTypes
enigma0Z Dec 13, 2024
09d60df
Update snapshots and fix beforeEach()
enigma0Z Dec 13, 2024
3e7cc83
Update snapshot
enigma0Z Dec 13, 2024
097a317
Enable new arch
enigma0Z Dec 13, 2024
d721d15
Disable old mock (probalby remove this before merging)
enigma0Z Dec 13, 2024
2023a14
Update extraneous react native deps
enigma0Z Dec 13, 2024
4cd2ea6
(Temporary?) Disable bugged tests
enigma0Z Dec 13, 2024
dc15607
Remove @types/react-native per expo doctor's recommendation
enigma0Z Dec 13, 2024
10403f0
npm audit fix --force
enigma0Z Dec 13, 2024
374d5d7
Merge branch 'master' into feature/update-expo-to-52
enigma0Z Dec 13, 2024
ba76e9b
Merge branch 'feature/update-expo-to-52' of github.com:enigma0Z/jelly…
enigma0Z Dec 13, 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');

// 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(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn();
});
});
});
11 changes: 8 additions & 3 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"expo": {
"newArchEnabled": true,
"name": "Jellyfin",
"slug": "jellyfin-expo",
"owner": "jellyfin",
"privacy": "unlisted",
"platforms": [
"ios"
],
"version": "1.6.0",
"version": "1.7.0",
"orientation": "default",
"primaryColor": "#00a4dc",
"backgroundColor": "#101010",
Expand Down Expand Up @@ -38,6 +38,11 @@
]
}
},
"description": "Jellyfin Mobile"
"description": "Jellyfin Mobile",
"plugins": [
"expo-asset",
"expo-font",
"expo-localization"
]
}
}
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