Conversation
Replaces the broken api.content.history (Sonos, album-only) with a new Azure Function that queries the Cosmos events container for the current user's queue history from the past 7 days. - New Azure Function /api/recently-played — deduplicates in-memory, returns top 20 tracks / 10 artists / 10 albums sorted by most-recent - IPC handler history:recent + preload bridge fetchRecentlyPlayed - useRecentlyPlayed hook — fetches by displayName, converts to SonosItem so CardRow/useOpenItem can render and navigate them - HomePanel: three conditional rows (Artists, Albums, Tracks) replacing the single dead Recently Played row Needs: cd server && npm run deploy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getName() prefers item.name over item.title — toAlbumItem was setting name: a.artist which caused the album row to display artist names. Also wire artist field on both album and track items for subtitle display. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace CardRow with a bespoke side-by-side layout: album list (left) and circular artist grid (right), max 9 artists capped at 3×N columns - Artist images fetched in parallel via useQueries; circular cards with fade-in glow on hover; artist names shown below circles - Album rows: draggable (application/sonos-item-list), add-to-queue button visible on hover, grab cursor - When no albums present, artist grid spans full width with up to 5 cols - User picker inline in section title; locks with tooltip when Quedle not yet completed; "Nothing queued in the last 7 days" empty state - Loading skeleton mirrors the real layout with staggered pulse animation - Server: returns availableUsers (last 30 days) alongside recent data - Artists capped at 9 (3×3 max); CardRow artist cards now circular with no add-to-queue button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add /profile/:userName route with ProfilePanel showing recently played, all-time top tracks/artists/albums, and Queuedle tier + stat grid - Add useUserStats hook wrapping fetchStats alltime per user - Add TopNav profile button (Contact icon) for current user - Make attribution usernames in queue rows clickable to profile - Make all leaderboard usernames clickable to profile - Make all Queuedle panel leaderboard names clickable to profile - Add profile icon button to HomePanel user picker dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Implement comprehensive playlist management with dedicated UI panel. - Allow users to create, view, add tracks to, and manage their own playlists. - Introduce a global context menu for tracks to play next, queue, or add to playlist. - Integrate track context menu across album, artist, queue, and search track lists. - Display user playlists on home and profile panels with a create playlist option. - Add backend APIs and storage for playlist data and cover images.
- Extract getPlaylistColor, createDragGhost, PlaylistCard into shared modules - Move @Keyframes skelPulse to global.css (was duplicated in 3 modules) - Profile chip in TopNav navigates directly to profile page; sign-in shows name-entry popover - Sign out button moved to profile page, inline with play count (X plays | Sign out) - Playlists section moved above Recently Played on profile page with + New button - Playlist cards now show cover art via imageUrl from list endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… grid - Profile picture upload: Azure Blob + Cosmos profiles container, camera emoji overlay, shown in TopNav chip - Top tracks: 2-column 5×5 grid with multi-select, drag-to-queue, clickable artist/album links - Queuedle rank promoted to header hero block with tier icon - Stats fetch count raised to 25 (server accepts ?count param) - Top artists/albums displayed at 110px for compact fit - Playlists section now wraps as CSS grid instead of horizontal scroll Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds the “social layer” to TrueTunes by introducing a Cosmos-backed playlist system (including auto-created Favourites), user profiles (with image upload), a People row, and a “Recently Played (last 7 days)” home section, wiring the new APIs through Electron IPC and exposing them in the renderer with new panels/hooks, context menus, and toast-based error feedback.
Changes:
- Added new Azure Functions + Cosmos/Blob infra to support playlists, favourites, profiles, people list, and recently played aggregation.
- Updated Electron preload + main IPC handlers to expose new playlist/profile/history APIs to the renderer (including stats
countsupport). - Added new renderer UI (Profile/Playlist panels, People row, context menu, toasts), shared utilities, and accompanying tests/styles.
Minimal code changes to address review findings (recommended):
- Fix Cosmos “not found” handling where
item.read()currently won’t reachif (!resource)branches (notably favourites ensure and playlist get/delete).
Files:server/src/functions/favourites-ensure.ts,server/src/functions/playlist-get.ts,server/src/functions/playlist-delete.ts - Align “owner-only remove” behavior with the PR description by enforcing owner-only removal server-side.
Files:server/src/functions/playlist-remove-track.ts - Make reorder mutation update caches consistently (invalidate playlist + playlist list queries, or apply server response including
updatedAt).
Files:renderer/src/components/PlaylistPanel.tsx, (optionally)renderer/src/hooks/usePlaylists.ts - Validate/clamp
daysin recently played and address the users-list N+1 profile reads if this endpoint is expected to scale.
Files:server/src/functions/recently-played.ts,server/src/functions/users-list.ts - Fix minor tooltip typo (“Quedle” → “Queuedle”).
Files:renderer/src/components/HomePanel.tsx
Tests to add/update (targeted):
- Add a
PlaylistPaneltest covering successful reorder (verifying expected query invalidations orupdatedAtupdate behavior).
Files:renderer/src/components/__tests__/PlaylistPanel.test.tsx
Tradeoffs (high-level):
- Batch-fetching profiles in
users-listreduces RU/latency vs. simple per-user reads (current approach is simpler but scales poorly). - Cache invalidation after reorder is slightly more network traffic but keeps “sorted by recently updated” consistent and avoids UI drift.
Reviewed changes
Copilot reviewed 73 out of 74 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/preload.ts | Exposes new IPC APIs (playlists, profiles, recently played, stats count). |
| src/main.ts | Adds IPC handlers for playlist/profile/history endpoints + stats count wiring. |
| server/src/lib/withOCC.ts | Introduces optimistic concurrency read-modify-write helper for Cosmos mutations. |
| server/src/lib/getContainer.ts | Adds cached Cosmos container accessor for playlists. |
| server/src/functions/users-list.ts | Adds People-row backing API (queued users + profile images). |
| server/src/functions/stats.ts | Adds count query param for variable-length stats results. |
| server/src/functions/recently-played.ts | Adds recently played aggregation + available users for picker. |
| server/src/functions/profile-upload-image.ts | Adds profile image upload to Blob + profile doc upsert. |
| server/src/functions/profile-get.ts | Adds profile fetch endpoint. |
| server/src/functions/playlist-upload-image.ts | Adds playlist cover upload to Blob + Cosmos patch. |
| server/src/functions/playlist-update.ts | Adds playlist rename/publicity update with OCC. |
| server/src/functions/playlist-reorder.ts | Adds track reorder mutation with OCC. |
| server/src/functions/playlist-remove-track.ts | Adds track removal mutation with OCC. |
| server/src/functions/playlist-list.ts | Adds playlist listing (owned/joined) endpoint for UI lists. |
| server/src/functions/playlist-join.ts | Adds join/leave mutation for public playlists with OCC. |
| server/src/functions/playlist-get.ts | Adds playlist detail fetch endpoint. |
| server/src/functions/playlist-delete.ts | Adds playlist deletion endpoint with favourites protection. |
| server/src/functions/playlist-create.ts | Adds playlist creation endpoint. |
| server/src/functions/playlist-add-track.ts | Adds track add mutation with OCC + addedBy/addedAt stamping. |
| server/src/functions/favourites-ensure.ts | Adds first-login favourites auto-creation endpoint. |
| server/azuredeploy.json | Provisions new Cosmos containers + playlist-images blob container. |
| renderer/src/types/globals.d.ts | Adds TS types for playlists/profiles/recently-played + preload API surface. |
| renderer/src/test/setup.ts | Extends window.sonos mocks for new preload APIs. |
| renderer/src/styles/TopNav.module.css | Adds profile chip/popover styles. |
| renderer/src/styles/Toast.module.css | Adds toast notification styles. |
| renderer/src/styles/QueueSidebar.module.css | Adds clickable attribution username styling. |
| renderer/src/styles/Queuedle.module.css | Adds clickable username button styling in Queuedle leaderboard. |
| renderer/src/styles/ProfilePanel.module.css | New profile page styling. |
| renderer/src/styles/PlaylistPanel.module.css | New playlist detail page styling (drag reorder, members, actions). |
| renderer/src/styles/PlayerBar.module.css | Adds favourited heart button styling. |
| renderer/src/styles/LeaderboardPanel.module.css | Adds clickable username button styling. |
| renderer/src/styles/HomePanel.module.css | Adds People row, playlists row, and recently played layouts/styles. |
| renderer/src/styles/global.css | Adds shared @keyframes skelPulse. |
| renderer/src/styles/ContextMenu.module.css | Adds context menu + submenu styling. |
| renderer/src/styles/CardRow.module.css | Adjusts CardRow padding/margins for updated layouts. |
| renderer/src/lib/playlistColor.ts | Adds deterministic playlist gradient helper. |
| renderer/src/lib/dragHelpers.ts | Extracts reusable drag ghost helper. |
| renderer/src/hooks/useUserStats.ts | Adds user stats hook (count-aware) for profile pages. |
| renderer/src/hooks/useUsers.ts | Adds users hook for People row. |
| renderer/src/hooks/useUserProfile.ts | Adds user profile hook + invalidation helper. |
| renderer/src/hooks/useRecentlyPlayed.ts | Adds recently played hook + artist image enrichment queries. |
| renderer/src/hooks/usePlaylists.ts | Adds playlists/favourites hooks incl. optimistic favourite toggle + invalidations. |
| renderer/src/hooks/useNowPlaying.ts | Exposes current track identity needed for favouriting. |
| renderer/src/hooks/tests/usePlaylists.test.ts | Adds tests for favourites toggle + ensure favourites query config. |
| renderer/src/components/TopNav.tsx | Replaces name popover with profile chip + sign-in flow and profile nav. |
| renderer/src/components/search/SearchResults.tsx | Adds context menu integration + shared drag ghost helper. |
| renderer/src/components/queuedle/QueuedlePanel.tsx | Makes leaderboard usernames link to profiles. |
| renderer/src/components/queue/QueueSidebar.tsx | Uses shared drag ghost helper. |
| renderer/src/components/queue/DraggableQueueRow.tsx | Adds track context menu + clickable attribution user → profile. |
| renderer/src/components/queue/tests/DraggableQueueRow.test.tsx | Updates attribution test for new clickable username element. |
| renderer/src/components/ProfilePanel.tsx | Adds new profile page UI (stats, playlists, avatar upload). |
| renderer/src/components/PlaylistPanel.tsx | Adds new playlist detail UI (join/leave, rename, delete, upload, reorder). |
| renderer/src/components/PlayerBar.tsx | Adds Favourites heart toggle (optimistic) tied to now playing track. |
| renderer/src/components/LeaderboardPanel.tsx | Makes usernames link to profiles while preserving drill-down behavior. |
| renderer/src/components/HomePanel.tsx | Adds People row, playlists row, and “Recently Played — Last 7 Days” section with picker gating. |
| renderer/src/components/common/Toast.tsx | Adds ToastProvider/useToast for mutation failure feedback. |
| renderer/src/components/common/PlaylistCard.tsx | Adds shared playlist card component for home/profile lists. |
| renderer/src/components/common/PlaylistCard.module.css | Adds playlist card styling. |
| renderer/src/components/common/MediaRow.tsx | Adds optional onContextMenu plumbing to row component. |
| renderer/src/components/common/MediaCard.tsx | Adds circular option (used for artists). |
| renderer/src/components/common/MediaCard.module.css | Adds circular art styling. |
| renderer/src/components/common/ContextMenu.tsx | Adds global track context menu + playlist submenu + create playlist dialog. |
| renderer/src/components/common/tests/CreatePlaylistDialog.test.tsx | Adds tests for create playlist dialog (with/without pending track, error). |
| renderer/src/components/CardRow.tsx | Adds artist circular cards + configurable card size. |
| renderer/src/components/artist/TopSongRow.tsx | Adds track context menu on artist top song rows. |
| renderer/src/components/artist/HeroTrackRow.tsx | Adds track context menu on artist hero rows. |
| renderer/src/components/artist/ArtistHero.tsx | Uses shared drag ghost helper. |
| renderer/src/components/album/AlbumTrackRow.tsx | Adds track context menu on album track rows. |
| renderer/src/components/album/AlbumPanel.tsx | Uses shared drag ghost helper. |
| renderer/src/components/tests/TopNav.test.tsx | Updates tests for profile chip / sign-in popover behavior. |
| renderer/src/components/tests/PlaylistPanel.test.tsx | Adds tests for optimistic remove track + rollback in playlist panel. |
| renderer/src/components/tests/LeaderboardPanel.test.tsx | Updates drill-down tests to account for username button navigation. |
| renderer/src/components/tests/HomePanel.test.tsx | Updates home tests for recently played hook integration. |
| renderer/src/App.tsx | Adds routes for profile/playlist pages + wraps app in Toast/ContextMenu providers; ensures favourites on login; updates Home/PlayerBar props. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+28
to
+41
| export async function profileUploadImageHandler( | ||
| request: HttpRequest, | ||
| context: InvocationContext, | ||
| ): Promise<HttpResponseInit> { | ||
| const userName = request.params['userName']; | ||
| if (!userName) return { status: 400, jsonBody: { error: 'userName required' } }; | ||
|
|
||
| const storageConn = process.env['STORAGE_CONNECTION_STRING']; | ||
| const cosmosConn = process.env['COSMOS_CONNECTION_STRING']; | ||
| const dbName = process.env['COSMOS_DATABASE'] ?? 'truetunes'; | ||
|
|
||
| if (!storageConn || !cosmosConn) { | ||
| return { status: 500, jsonBody: { error: 'Storage or Cosmos not configured' } }; | ||
| } |
Comment on lines
+31
to
+34
| (doc) => { | ||
| if (doc.owner !== userName && !doc.members.includes(userName)) { | ||
| throw Object.assign(new Error('Not a member of this playlist'), { statusCode: 403 }); | ||
| } |
Comment on lines
+15
to
+18
| const { name, isPublic = false, owner } = body; | ||
| if (!name || !owner) { | ||
| return { status: 400, jsonBody: { error: 'name and owner required' } }; | ||
| } |
Comment on lines
+46
to
+50
| // Verify ownership before uploading to storage | ||
| const cosmos = new CosmosClient(cosmosConn); | ||
| const { resource: playlistDoc } = await cosmos.database(dbName).container('playlists').item(id, id).read(); | ||
| if (!playlistDoc) return { status: 404, jsonBody: { error: 'Playlist not found' } }; | ||
| if (playlistDoc.owner !== userName) return { status: 403, jsonBody: { error: 'Only the owner can change the playlist image' } }; |
Comment on lines
+64
to
+66
| const days = parseInt(request.query.get('days') ?? '7', 10); | ||
| const startMs = Date.now() - days * 24 * 60 * 60 * 1000; | ||
|
|
Comment on lines
+33
to
+36
| if (owner) { | ||
| query = { | ||
| query: 'SELECT c.id, c.name, c.owner, c.isPublic, c.isFavourites, c.members, c.imageUrl, c.createdAt, c.updatedAt, ARRAY_LENGTH(c.tracks) AS trackCount FROM c WHERE c.owner = @owner ORDER BY c.updatedAt DESC', | ||
| parameters: [{ name: '@owner', value: owner }], |
Comment on lines
+13
to
+20
| try { | ||
| const container = getPlaylistContainer(); | ||
|
|
||
| const { resource } = await container.item(id, id).read(); | ||
|
|
||
| if (!resource) { | ||
| return { status: 404, jsonBody: { error: 'Playlist not found' } }; | ||
| } |
Comment on lines
+18
to
+22
| // Return existing | ||
| const { resource } = await container.item(id, id).read(); | ||
| if (resource) { | ||
| return { jsonBody: resource, headers: { 'Access-Control-Allow-Origin': '*' } }; | ||
| } |
Comment on lines
+28
to
+31
| const { resource } = await container.item(id, id).read(); | ||
| if (!resource) { | ||
| return { status: 404, jsonBody: { error: 'Playlist not found' } }; | ||
| } |
Comment on lines
+14
to
+20
| const container = getPlaylistContainer(); | ||
|
|
||
| const { resource } = await container.item(id, id).read(); | ||
|
|
||
| if (!resource) { | ||
| return { status: 404, jsonBody: { error: 'Playlist not found' } }; | ||
| } |
- playlist-create: add name trim/empty/max-length validation consistent with playlist-update - playlist-remove-track: restrict to owner only (was allowing any member) - recently-played: clamp and validate days param (1–30, return 400 on invalid) - playlist-upload-image: move Cosmos ownership check inside try/catch - PlaylistPanel: invalidate playlist detail and list queries after successful reorder - HomePanel: fix typo Quedle → Queuedle in picker tooltip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This release brings the social layer of TrueTunes to life — playlists you can build and share, a personal Favourites playlist, full user profile pages, a People row on the home screen, and the recently played feature that kicked the branch off.
Playlists
Full playlist system backed by Azure Cosmos DB with optimistic concurrency control on all mutations.
What's new:
/playlist/:id) with full track table, member list, and metadataServer functions added:
playlist-create,playlist-get,playlist-list,playlist-add-track,playlist-remove-track,playlist-reorder,playlist-update,playlist-delete,playlist-join,playlist-upload-imageInfrastructure: new
playlistsCosmos container andplaylist-imagesblob container provisioned via ARM template.Favourites
favourites-ensureAzure Function)useFavouritehook withisFavouritedstate derived from the cached playlist, full add/remove with rollbackUser Profiles
/profile/:userNameshowing top tracks, top artists, top albums, playlists, and Queuedle rankuseUserProfileanduseUserStatshooks;profile-getandprofile-upload-imageAzure FunctionsPeople Row (Home Page)
users-listAzure Function queries the events container withGROUP BYto get last-queued timestamp per user, joins with profiles for imagesRecently Played
recently-playedAzure Function aggregates Cosmos events per userContext Menu
ContextMenuProviderwraps the app shell;useTrackContextMenuhook exposesshowTrackMenuto any componentCreatePlaylistDialoginline in the context menu flow for creating a new playlist and immediately adding the track to itToast Notifications
ToastProvider/useToastfor non-blocking error feedback on all mutation failures (add track, remove track, rename, delete, join, reorder, upload image)Code quality & bug fixes
playlistFetchhelper that throws on non-2xx responses, so renderercatchblocks and optimistic rollbacks actually fire on server errors (previously handlers returned{ error: '...' }silently)PlaylistPanelrename input using acancelRenamerefgetPlaylistColor,createDragGhost,PlaylistCardcomponent, and@keyframes skelPulseextracted to canonical locations, eliminating duplication across HomePanel, ProfilePanel, and PlaylistPaneluseEnsureFavouritesretry logic now works correctly (handler throws instead of returning error object)handleJoinin PlaylistPanel had no error handling — fixed with contextual toastuseFavouritetoggle invalidates the playlist list query after success so Favourites track count stays accurateTest coverage
New test files:
usePlaylists.test.ts—useFavouriteadd/remove happy paths, rollback paths,useEnsureFavouritesstale time and retry configPlaylistPanel.test.tsx—handleRemoveTrackoptimistic update and rollbackCreatePlaylistDialog.test.tsx— create without track, create with pending track, server error inline displayTopNav.test.tsx— updated to match new profile chip navigation behaviour722 tests passing, full typecheck clean.