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:
@@ -4,6 +4,7 @@ import Link from 'next/link';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared';
|
import { sortFeed, type FeedItemDTO, type ProfileDTO } from '@musicshare/shared';
|
||||||
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
import { NowPlayingCard } from '@/components/NowPlayingCard';
|
||||||
|
import { SharingControls } from '@/components/SharingControls';
|
||||||
import { UserSearch } from '@/components/UserSearch';
|
import { UserSearch } from '@/components/UserSearch';
|
||||||
import { api, fetcher, useMe } from '@/lib/api';
|
import { api, fetcher, useMe } from '@/lib/api';
|
||||||
import { useRealtime } from '@/lib/useRealtime';
|
import { useRealtime } from '@/lib/useRealtime';
|
||||||
@@ -53,6 +54,7 @@ function Feed() {
|
|||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
<UserSearch />
|
<UserSearch />
|
||||||
|
<SharingControls compact />
|
||||||
{mine?.nowPlaying && (
|
{mine?.nowPlaying && (
|
||||||
<section className="stack">
|
<section className="stack">
|
||||||
<h2 className="section-label">You</h2>
|
<h2 className="section-label">You</h2>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type NowPlayingProvider,
|
type NowPlayingProvider,
|
||||||
type TrackSearchResultDTO,
|
type TrackSearchResultDTO,
|
||||||
} from '@musicshare/shared';
|
} from '@musicshare/shared';
|
||||||
|
import { SharingControls } from '@/components/SharingControls';
|
||||||
import { api, fetcher, useMe } from '@/lib/api';
|
import { api, fetcher, useMe } from '@/lib/api';
|
||||||
import { timeAgo } from '@musicshare/shared';
|
import { timeAgo } from '@musicshare/shared';
|
||||||
import { useDebounced } from '@/lib/useDebounced';
|
import { useDebounced } from '@/lib/useDebounced';
|
||||||
@@ -32,6 +33,7 @@ function Settings() {
|
|||||||
<div className="stack">
|
<div className="stack">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<ProfileForm me={me} />
|
<ProfileForm me={me} />
|
||||||
|
<SharingControls />
|
||||||
<Connections />
|
<Connections />
|
||||||
<FeaturedPicker />
|
<FeaturedPicker />
|
||||||
<DangerZone />
|
<DangerZone />
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function NowPlayingCard({
|
|||||||
<span className="row">
|
<span className="row">
|
||||||
<span className={`status ${np.isPlaying ? 'status-live' : ''}`}>
|
<span className={`status ${np.isPlaying ? 'status-live' : ''}`}>
|
||||||
{np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`}
|
{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>
|
</span>
|
||||||
{onTogglePin && (
|
{onTogglePin && (
|
||||||
<button
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user