Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 21 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ActivityIndicator,
Linking,
Modal,
Pressable,
StyleSheet,
Text,
useColorScheme,
Expand All @@ -29,7 +30,7 @@ function LegacyUpgradeModal({ theme, visible = false }) {
return (
<Modal
animationType="fade"
onRequestClose={() => {}}
onRequestClose={() => Episodes.continue_without_legacy_upgrade()}
presentationStyle="overFullScreen"
statusBarTranslucent
transparent
Expand All @@ -51,6 +52,15 @@ function LegacyUpgradeModal({ theme, visible = false }) {
<Text style={[styles.upgradeTitle, { color: theme.colors.ink }]}>
Upgrading previous Wavelength recordings...
</Text>
<Pressable
accessibilityRole="button"
onPress={() => Episodes.continue_without_legacy_upgrade()}
style={({ pressed }) => [styles.continueButton, { opacity: pressed ? 0.68 : 1 }]}
>
<Text style={[styles.continueButtonText, { color: theme.colors.accent_strong }]}>
Continue
</Text>
</Pressable>
</View>
</View>
</Modal>
Expand Down Expand Up @@ -120,6 +130,16 @@ function App() {
}

const styles = StyleSheet.create({
continueButton: {
alignItems: 'center',
justifyContent: 'center',
minHeight: 44,
paddingHorizontal: 20,
},
continueButtonText: {
fontSize: 17,
fontWeight: '600',
},
loadingScreen: {
flex: 1,
alignItems: 'center',
Expand Down
23 changes: 21 additions & 2 deletions src/__tests__/App.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ jest.mock('react-native', () => ({
getInitialURL: jest.fn(async () => null),
},
Modal: 'Modal',
Pressable: 'Pressable',
StyleSheet: {
create: styles => styles,
flatten: style => Object.assign({}, ...[style].flat().filter(Boolean)),
},
Text: 'Text',
View: 'View',
Expand Down Expand Up @@ -61,6 +63,7 @@ jest.mock('../theme/wavelengthTheme', () => ({
is_dark: false,
colors: {
accent: '#ff8800',
accent_strong: '#cc6600',
canvas: '#fffaf0',
ink: '#24180d',
line: '#eee',
Expand All @@ -70,6 +73,7 @@ jest.mock('../theme/wavelengthTheme', () => ({
}));

jest.mock('../stores/Episodes', () => ({
continue_without_legacy_upgrade: jest.fn(),
is_upgrading_legacy: false,
}));

Expand All @@ -80,14 +84,15 @@ jest.mock('../stores/Auth', () => ({
handle_open_url: (...args) => mock_handle_open_url(...args),
hydrate: jest.fn(async () => {}),
is_hydrating: false,
is_signed_in: () => false,
is_signed_in: jest.fn(() => false),
is_signing_in: true,
}));

const React = require('react');
const { render } = require('@testing-library/react-native');
const { fireEvent, render } = require('@testing-library/react-native');
const App = require('../App').default;
const Auth = require('../stores/Auth');
const Episodes = require('../stores/Episodes');

describe('App auth callback URLs', () => {
beforeEach(async () => {
Expand All @@ -96,6 +101,7 @@ describe('App auth callback URLs', () => {
Auth.can_handle_open_url.mockReset();
Auth.can_handle_open_url.mockReturnValue(true);
Auth.is_signing_in = true;
Auth.is_signed_in.mockReturnValue(false);

await render(React.createElement(App));
});
Expand All @@ -109,3 +115,16 @@ describe('App auth callback URLs', () => {
expect(mock_handle_open_url).toHaveBeenCalledWith(callback_url);
});
});

describe('App legacy upgrade', () => {
test('Continue delegates to the store to stop waiting for the upgrade', async () => {
Auth.is_signed_in.mockReturnValue(true);
Episodes.is_upgrading_legacy = true;

const { getByText } = await render(React.createElement(App));

await fireEvent.press(getByText('Continue'));

expect(Episodes.continue_without_legacy_upgrade).toHaveBeenCalledTimes(1);
});
});
108 changes: 87 additions & 21 deletions src/stores/Episodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ import Auth from './Auth';
import Posts from './Posts';
import Tokens from './Tokens';

async function upgrade_legacy_episode(legacy_episode) {
const LEGACY_UPGRADE_TIMEOUT_MILLIS = 30_000;

async function upgrade_legacy_episode(legacy_episode, upgrade) {
const existing = await read_migrated_episode(
legacy_episode.id,
legacy_episode.clips.length,
);

if (upgrade.is_cancelled) {
return null;
}

if (existing) {
await delete_legacy_episode(legacy_episode.id);
return existing;
Expand All @@ -51,9 +57,19 @@ async function upgrade_legacy_episode(legacy_episode) {
for (const clip of legacy_episode.clips) {
const converted = await normalize_imported_audio(clip.uri);
converted_clips.push(converted);

// Native conversion can finish after we have stopped waiting for it.
if (upgrade.is_cancelled) {
return null;
}
}

const migrated = await save_migrated_episode(legacy_episode, converted_clips);

if (upgrade.is_cancelled) {
return null;
}

await delete_legacy_episode(legacy_episode.id);

return migrated;
Expand Down Expand Up @@ -136,6 +152,7 @@ const Episodes = types
export_fingerprints: {},
is_loading: false,
is_upgrading_legacy: false,
legacy_upgrade: null,
}))
.actions(self => ({
apply_episode_snapshot(snapshot) {
Expand All @@ -149,42 +166,91 @@ const Episodes = types
},

refresh: flow(function* () {
if (self.is_loading) {
return;
}

self.is_loading = true;

try {
if (!self.did_check_for_legacy) {
self.did_check_for_legacy = true;
try {
const loaded_episodes = yield list_episodes();
applySnapshot(self.episodes, loaded_episodes);
} catch (error) {
console.warn('Could not load recordings:', error);
}

const legacy_episodes = yield list_legacy_episodes();
self.did_hydrate = true;
yield self.upgrade_legacy_recordings();
} finally {
self.is_loading = false;
}
}),

upgrade_legacy_recordings: flow(function* () {
if (self.did_check_for_legacy) {
return;
}

if (legacy_episodes.length > 0) {
self.is_upgrading_legacy = true;
self.did_check_for_legacy = true;

const upgrade = { is_cancelled: false, cancel: null };
const cancelled = new Promise(resolve => {
upgrade.cancel = () => {
upgrade.is_cancelled = true;
resolve();
};
});
self.legacy_upgrade = upgrade;

const timeout = setTimeout(() => {
console.warn('Legacy recording upgrade timed out.');
self.continue_without_legacy_upgrade();
}, LEGACY_UPGRADE_TIMEOUT_MILLIS);
Comment on lines +206 to +209

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid cancelling healthy migrations after a fixed total timeout

This single timer covers the entire legacy scan and every sequential clip conversion, but normalize_imported_audio analyzes and transcodes each complete audio file. On a slower device, a long recording, or a library containing several recordings, valid work can exceed 30 seconds; the timeout then marks the upgrade cancelled, discards a late conversion, and did_check_for_legacy prevents another attempt for the lifetime of the process. A clip that consistently takes over 30 seconds can therefore remain inaccessible on every launch even though conversion is making progress, so the timeout should detect inactivity or reset around individual progress rather than cap the whole migration.

Useful? React with 👍 / 👎.


try {
const legacy_episodes = yield Promise.race([list_legacy_episodes(), cancelled]);

if (!upgrade.is_cancelled && legacy_episodes.length > 0) {
self.is_upgrading_legacy = true;

for (const legacy_episode of legacy_episodes) {
try {
for (const legacy_episode of legacy_episodes) {
try {
yield upgrade_legacy_episode(legacy_episode);
} catch (error) {
// Leave the old folder intact so a future launch can retry it.
}
const migrated = yield Promise.race([
upgrade_legacy_episode(legacy_episode, upgrade),
cancelled,
]);

if (!upgrade.is_cancelled) {
self.apply_episode_snapshot(migrated);
}
} finally {
self.is_upgrading_legacy = false;
} catch (error) {
// Leave the old folder intact so a future launch can retry it.
console.warn('Could not upgrade legacy recording:', legacy_episode.id, error);
}

if (upgrade.is_cancelled) {
break;
}
}
}

const loaded_episodes = yield list_episodes();
applySnapshot(self.episodes, loaded_episodes);
self.did_hydrate = true;
} catch (error) {
console.warn('Could not upgrade legacy recordings:', error);
} finally {
clearTimeout(timeout);
self.is_upgrading_legacy = false;
self.did_hydrate = true;
self.legacy_upgrade = null;
}

self.is_loading = false;
}),

continue_without_legacy_upgrade() {
if (self.legacy_upgrade) {
self.legacy_upgrade.cancel();
}

self.is_upgrading_legacy = false;
},

refresh_episode: flow(function* (episode_id = '') {
const snapshot = yield read_episode(episode_id);

Expand Down
Loading