Add pause controls and app attribution to the web app

Settings gets a Sharing card (pause 1 hour / until tomorrow / until
turned back on); the feed shows a resume banner while paused. Now-playing
cards say which app a phone reported, e.g. "via Spotify".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-03 17:37:30 -04:00
co-authored by Claude Opus 5.5
parent 70fc7efbbe
commit 23faffc251
4 changed files with 67 additions and 1 deletions
+2
View File
@@ -4,6 +4,7 @@ import Link from 'next/link';
import useSWR from 'swr';
import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared';
import { NowPlayingCard } from '@/components/NowPlayingCard';
import { SharingControls } from '@/components/SharingControls';
import { UserSearch } from '@/components/UserSearch';
import { api, fetcher, useMe } from '@/lib/api';
import { useRealtime } from '@/lib/useRealtime';
@@ -53,6 +54,7 @@ function Feed() {
return (
<div className="stack">
<UserSearch />
<SharingControls compact />
{mine?.nowPlaying && (
<section className="stack">
<h2 className="section-label">You</h2>
+2
View File
@@ -11,6 +11,7 @@ import {
type NowPlayingProvider,
type TrackSearchResultDTO,
} from '@musicshare/shared';
import { SharingControls } from '@/components/SharingControls';
import { api, fetcher, useMe } from '@/lib/api';
import { timeAgo } from '@musicshare/shared';
import { useDebounced } from '@/lib/useDebounced';
@@ -32,6 +33,7 @@ function Settings() {
<div className="stack">
<h1>Settings</h1>
<ProfileForm me={me} />
<SharingControls />
<Connections />
<FeaturedPicker />
<DangerZone />
+1 -1
View File
@@ -31,7 +31,7 @@ export function NowPlayingCard({
<span className="row">
<span className={`status ${np.isPlaying ? 'status-live' : ''}`}>
{np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`}
<span className="muted"> via {SERVICE_LABELS[np.source]}</span>
<span className="muted"> via {np.sourceApp ?? SERVICE_LABELS[np.source]}</span>
</span>
{onTogglePin && (
<button
@@ -0,0 +1,62 @@
'use client';
import { useState } from 'react';
import { pausedUntilLabel, type MeDTO } from '@musicshare/shared';
import { api, useMe } from '@/lib/api';
/** Pause/resume sharing. Compact mode is the one-line banner on the feed. */
export function SharingControls({ compact }: { compact?: boolean }) {
const { me, mutate } = useMe();
const [busy, setBusy] = useState(false);
if (!me) return null;
const set = async (pause: '1h' | '8h' | 'indefinitely' | null) => {
setBusy(true);
try {
await mutate(await api<MeDTO>('/me/sharing', { method: 'PUT', body: { pause } }), { revalidate: false });
} finally {
setBusy(false);
}
};
if (compact) {
if (!me.sharingPausedUntil) return null;
return (
<p className="notice row">
<span className="grow">Sharing is paused {pausedUntilLabel(me.sharingPausedUntil)}.</span>
<button className="btn btn-small btn-primary" onClick={() => set(null)} disabled={busy}>
Resume
</button>
</p>
);
}
return (
<section className="card form">
<h2>Sharing</h2>
{me.sharingPausedUntil ? (
<>
<p className="muted">Paused {pausedUntilLabel(me.sharingPausedUntil)}. Friends don’t see what you play, and nothing is added to your history.</p>
<button className="btn btn-primary" onClick={() => set(null)} disabled={busy} style={{ alignSelf: 'flex-start' }}>
Resume sharing
</button>
</>
) : (
<>
<p className="muted">Friends see what you’re playing. Pause any time.</p>
<div className="row">
<button className="btn" onClick={() => set('1h')} disabled={busy}>
Pause for 1 hour
</button>
<button className="btn" onClick={() => set('8h')} disabled={busy}>
Until tomorrow
</button>
<button className="btn" onClick={() => set('indefinitely')} disabled={busy}>
Until I turn it back on
</button>
</div>
</>
)}
</section>
);
}