From fa25ce71f2cd83afe01a2823c13ae15e046adbde Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Thu, 8 Oct 2026 01:22:46 -0700 Subject: [PATCH] fix(mobile): keep iOS queue index right when starting mid-list Co-Authored-By: Claude Opus 5.5 --- .../src/components/audio/AudioPlayer.tsx | 21 ++++----- .../audio/addTracksAroundCurrent.test.ts | 45 +++++++++++++++++++ .../audio/addTracksAroundCurrent.ts | 39 ++++++++++++++++ 3 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 packages/mobile/src/components/audio/addTracksAroundCurrent.test.ts create mode 100644 packages/mobile/src/components/audio/addTracksAroundCurrent.ts diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index ca15707e43b..2b79ac94bab 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -58,6 +58,7 @@ import { } from 'app/store/offline-downloads/slice' import { useChromecast } from './GoogleCast' +import { addTracksAroundCurrent } from './addTracksAroundCurrent' import { useSavePodcastProgress } from './useSavePodcastProgress' export const DEFAULT_IMAGE_URL = @@ -340,20 +341,14 @@ const useQueueSync = (isAudioSetup: boolean) => { // Sort by original index so RNTP order matches redux order resolved.sort((a, b) => a.i - b.i) - const before = resolved.filter(({ i }) => i < startIndex) - const after = resolved.filter(({ i }) => i > startIndex) + const before = resolved + .filter(({ i }) => i < startIndex) + .map(({ data }) => data) + const after = resolved + .filter(({ i }) => i > startIndex) + .map(({ data }) => data) - // Insert "before" tracks at position 0. Iterate from last to first so - // that earliest ends up at index 0 (each insert pushes prior entries - // right by one). - for (let j = before.length - 1; j >= 0; j--) { - await TrackPlayer.add(before[j].data, 0) - } - - // Append "after" tracks at the end - if (after.length > 0) { - await TrackPlayer.add(after.map(({ data }) => data)) - } + await addTracksAroundCurrent(TrackPlayer, before, after) } }, [makeTrackData] diff --git a/packages/mobile/src/components/audio/addTracksAroundCurrent.test.ts b/packages/mobile/src/components/audio/addTracksAroundCurrent.test.ts new file mode 100644 index 00000000000..d959697da84 --- /dev/null +++ b/packages/mobile/src/components/audio/addTracksAroundCurrent.test.ts @@ -0,0 +1,45 @@ +import { addTracksAroundCurrent } from './addTracksAroundCurrent' + +// Mirrors SwiftAudioEx 1.x QueueManager: an insert in front of the current +// item only shifts currentIndex when the queue already has more than one item. +const makeSwiftAudioExQueue = (current: string) => { + const queue = { items: [current], currentIndex: 0 } + const player = { + add: async (tracks: string[], index?: number) => { + const at = index ?? queue.items.length + if (queue.items.length > 1 && queue.currentIndex >= at) { + queue.currentIndex += tracks.length + } + queue.items.splice(at, 0, ...tracks) + }, + remove: async (indexes: number[]) => { + for (const index of [...indexes].sort((a, b) => b - a)) { + queue.items.splice(index, 1) + if (index < queue.currentIndex) queue.currentIndex -= 1 + } + } + } + return { queue, player } +} + +const cases: Array<[string, string[], string[]]> = [ + ['first track', [], ['b', 'c']], + ['middle track', ['a'], ['c']], + ['middle track with several before', ['a', 'b', 'c'], ['e', 'f']], + ['last track', ['a', 'b'], []], + ['last of two', ['a'], []], + ['only track', [], []] +] + +describe('addTracksAroundCurrent', () => { + it.each(cases)( + 'keeps order and current index for the %s', + async (_, before, after) => { + const { queue, player } = makeSwiftAudioExQueue('current') + await addTracksAroundCurrent(player, before, after) + + expect(queue.items).toEqual([...before, 'current', ...after]) + expect(queue.items[queue.currentIndex]).toBe('current') + } + ) +}) diff --git a/packages/mobile/src/components/audio/addTracksAroundCurrent.ts b/packages/mobile/src/components/audio/addTracksAroundCurrent.ts new file mode 100644 index 00000000000..5ebc2c89f54 --- /dev/null +++ b/packages/mobile/src/components/audio/addTracksAroundCurrent.ts @@ -0,0 +1,39 @@ +type QueuePlayer = { + add: (tracks: T[], insertBeforeIndex?: number) => Promise + remove: (indexes: number[]) => Promise +} + +/** + * Adds the tracks before and after the current track, which must already be + * the only item in the queue, so the final order is [...before, current, + * ...after]. + * + * On iOS, SwiftAudioEx only shifts its current index for an insert in front + * of the current track when the queue already holds more than one item. + * Inserting in front of a lone track leaves the index pointing one slot too + * early, so when the track ends the player replays it instead of advancing. + * To avoid that, the "after" tracks go in first, and when there are none a + * placeholder is appended for the duration of the insert. + */ +export const addTracksAroundCurrent = async ( + player: QueuePlayer, + before: T[], + after: T[] +) => { + if (after.length > 0) { + await player.add(after) + } + if (before.length === 0) return + + const needsPlaceholder = after.length === 0 + if (needsPlaceholder) { + await player.add([before[before.length - 1]]) + } + + await player.add(before, 0) + + if (needsPlaceholder) { + // Queue is now [...before, current, placeholder] + await player.remove([before.length + 1]) + } +}