// Read-only queue sheet for the Desktop Remote: the desktop's current +
// upcoming tracks, tap-to-play. Deliberately NOT the native queue — that reads
// the local playback session straight out of Room (drag-reorder, swipe-remove,
// multi-select), none of which applies to a remote snapshot. This is the only
// remaining React Native queue surface. It uses an INLINE BottomSheet because
// BottomSheetModal's portal does not work in this app's screen setups (see
// queue-tray-sheet gotcha).
import { useCallback, useEffect, useMemo } from 'react';
import { Pressable, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import BottomSheet, {
BottomSheetBackdrop,
type BottomSheetBackdropProps,
type BottomSheetHandleProps,
useBottomSheetScrollableCreator,
} from '@gorhom/bottom-sheet';
import { FlashList, type ListRenderItemInfo } from '@shopify/flash-list';
import { Text } from '@/components/Text';
import { radius, spacing } from '@/theme';
import { createThemedStyles, useColors } from '@/theme/themed';
import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { formatDuration } from '@/lib/format';
import { useDesktopRemoteStore } from '@/stores/desktopRemoteStore';
import type { DesktopRemoteQueueItem } from '@/types/desktopRemote';
import { QueueSheetHandle } from './QueueSheetHandle';
interface RemoteQueueSheetProps {
onClose: () => void;
embedded?: boolean;
}
export function RemoteQueueSheet({ onClose, embedded = false }: RemoteQueueSheetProps) {
const styles = useStyles();
const colors = useColors();
const ripple = useRipple();
const queue = useDesktopRemoteStore((s) => s.queue);
const snapPoints = useMemo(() => ['58%', '100%'], []);
const renderFlashListScrollComponent = useBottomSheetScrollableCreator();
// The SSE stream keeps the queue fresh while connected; refresh once on open
// in case the stream fell back to snapshot polling (which has no queue).
useEffect(() => {
void useDesktopRemoteStore.getState().refreshQueue();
}, []);
const items = queue?.items ?? [];
const upcomingCount = items.filter((item) => !item.isCurrent).length;
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
),
[]
);
const playItem = useCallback(
(item: DesktopRemoteQueueItem) => {
if (item.isCurrent) return;
void useDesktopRemoteStore.getState().playQueueItem(item.queueId);
onClose();
},
[onClose]
);
const renderItem = useCallback(
({ item }: ListRenderItemInfo) => (
playItem(item)}
disabled={item.isCurrent}
accessibilityRole="button"
accessibilityLabel={
item.isCurrent ? `Now playing: ${item.title}` : `Play ${item.title} on desktop`
}
>
{item.title || 'Unknown title'}
{item.artist || 'Unknown artist'}
{item.isCurrent ? (
) : item.durationSeconds !== null ? (
{formatDuration(item.durationSeconds)}
) : null}
),
[playItem, colors, styles, ripple]
);
const topSection = useMemo(
() => (
Desktop queue
{upcomingCount === 1 ? '1 song up next' : `${upcomingCount} songs up next`}
),
[colors.textTertiary, styles.headerRow, upcomingCount]
);
const content = (
item.queueId}
renderScrollComponent={embedded ? undefined : renderFlashListScrollComponent}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
The desktop queue is empty.
}
/>
);
const renderSheetHandle = useCallback(
(props: BottomSheetHandleProps) => (
{topSection}
),
[topSection]
);
if (embedded) {
return (
{topSection}
{content}
);
}
return (
{content}
);
}
const useStyles = createThemedStyles((colors) => ({
sheetBg: {
backgroundColor: colors.bgSecondary,
borderRadius: radius.lg,
},
handle: {
backgroundColor: colors.textTertiary,
},
embeddedRoot: {
flex: 1,
overflow: 'hidden',
},
headerRow: {
flexDirection: 'row',
alignItems: 'baseline',
justifyContent: 'space-between',
paddingHorizontal: spacing.lg,
paddingBottom: spacing.sm,
},
listContent: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
},
row: {
minHeight: 56,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
},
rowText: {
flex: 1,
minWidth: 0,
},
titleActive: {
color: colors.accent,
},
empty: {
paddingVertical: spacing.xl,
alignItems: 'center',
},
}));