diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 61 |
1 files changed, 60 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 38314e7..92dcb28 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -20,7 +20,9 @@ import { GroupPage } from './group-page.js'; import { SearchPage, ConnectionPool } from './search-page.js'; import { MusicPlayerBar } from './music-player.js'; import { NameModal } from './playlist-menu.js'; -import { saveQueueAsPlaylist } from './playlists.js'; +import { + saveQueueAsPlaylist, setPlaylistTransport, pullOnce, flushPush, +} from './playlists.js'; import { IndexingDock } from './index-dock.js'; import { SettingsPage } from './settings-page.js'; import { ProfilePage } from './profile-page.js'; @@ -752,6 +754,63 @@ function App() { const handleStopMusic = useCallback(() => setMusicQueue(null), []); + // ── Playlists reach a node through here ──────────────────────────────── + // + // `playlists.js` pushes on every write and needs a transport to push + // through; the shell is what owns connections. Preferring the group already + // open costs nothing, and dialing when there is none is a change of position + // that is worth stating: §7 refused dialing *at sign-in for a feature nobody + // had asked to use*. Somebody who has just made a playlist has asked, and one + // dial to save their work is cheaper than losing it. + useEffect(() => { + if (!user) { setPlaylistTransport(null); return undefined; } + setPlaylistTransport(async () => { + const gt = groupTransportRef.current; + const live = gt && gt.transportRef && gt.transportRef.current; + if (live && live.connected) return live; + // Bounded: an account with twenty groups, all offline, must not spend + // twenty ten-second timeouts on a background push. + for (const g of groups.slice(0, 3)) { + try { + const conn = await getMusicConnection(g.id); + if (conn && conn.transport && conn.transport.connected) return conn.transport; + } catch { /* try the next one */ } + } + return null; + }); + // A pending push has a second and a half to wait, and closing a laptop + // inside that window is not rare. `pagehide` covers a tab closing and a + // navigation; `visibilitychange` covers a phone being backgrounded, which + // on iOS is the only one of the two that reliably fires at all. + const flush = () => { flushPush().catch(() => {}); }; + const onHidden = () => { if (document.visibilityState === 'hidden') flush(); }; + window.addEventListener('pagehide', flush); + document.addEventListener('visibilitychange', onHidden); + + return () => { + window.removeEventListener('pagehide', flush); + document.removeEventListener('visibilitychange', onHidden); + setPlaylistTransport(null); + }; + }, [user, groups, getMusicConnection]); + + // Reconcile once per sign-in, whatever this browser already holds. + // + // Nothing went looking until a group's Music tab happened to be opened, which + // is not where anybody looks for a playlist — and bounding this to an empty + // device, as the first version did, left out the ordinary case: a phone that + // has nine playlists and is missing the tenth. + // + // `groups.length` moves as the sidebar fills, so the ref is what makes "once" + // mean once rather than once per group that arrives. + const pulledForRef = useRef(null); + useEffect(() => { + if (!user || !groups.length) return; + if (pulledForRef.current === user.userId) return; + pulledForRef.current = user.userId; + pullOnce(user.userId).catch(() => {}); + }, [user, groups.length]); + // Saving the queue is a shell-level action because the queue is: the player // bar outlives every page, and the account it belongs to is here. const [saveQueue, setSaveQueue] = useState(null); |