- Extract the shared markdown renderer/theme out of TextParticleView into a reusable MarkdownBody component (DRY). - PaperParticleView renders desktop-authored documents (title + markdown) with a length-based dwell. - FileParticleView shows name/size and a Download action that opens a signed URL via the OS. - Both wired into StreamView's render switch; FallbackParticleView is now a true catch-all for unknown/folder types only. Deferred (documented for a follow-up phase): composing papers/files from mobile, particle attachments + lightbox, and link previews in text. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01LqGPzXQ1AA9CqYHgCgmbtV
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import { ScrollView, Text, View } from 'react-native';
|
|
import type { Particle } from '@/api/types';
|
|
import { MarkdownBody } from '@/components/MarkdownBody';
|
|
import { useStreamSafeArea } from './stream-safe-area';
|
|
|
|
type PaperParticle = Extract<Particle, { type: 'paper' }>;
|
|
|
|
interface PaperParticleViewProps {
|
|
particle: PaperParticle;
|
|
paused: boolean;
|
|
onEnded: () => void;
|
|
onProgress: (ratio: number) => void;
|
|
}
|
|
|
|
// Papers are longer-form documents, so they read at the text cadence but with a
|
|
// higher cap — the reader can still scroll at their own pace while the timer
|
|
// ticks toward auto-advance.
|
|
const CHARS_PER_MINUTE = 1000;
|
|
const MIN_DURATION_S = 4;
|
|
const MAX_DURATION_S = 30;
|
|
const TICK_MS = 100;
|
|
|
|
function computeReadDuration(text: string): number {
|
|
const base = (text.length / CHARS_PER_MINUTE) * 60;
|
|
return Math.min(Math.max(base, MIN_DURATION_S), MAX_DURATION_S);
|
|
}
|
|
|
|
/**
|
|
* Read-only paper (document) view. Mirrors desktop's paper rendering: a title
|
|
* heading above markdown body, scrollable, with a length-based dwell timer.
|
|
*/
|
|
export function PaperParticleView({
|
|
particle,
|
|
paused,
|
|
onEnded,
|
|
onProgress,
|
|
}: PaperParticleViewProps) {
|
|
const { title, content } = particle.properties;
|
|
const safe = useStreamSafeArea();
|
|
const durationS = computeReadDuration(content);
|
|
const elapsedRef = useRef(0);
|
|
|
|
useEffect(() => {
|
|
elapsedRef.current = 0;
|
|
onProgress(0);
|
|
}, [particle.id, onProgress]);
|
|
|
|
useEffect(() => {
|
|
if (paused) return;
|
|
const interval = setInterval(() => {
|
|
elapsedRef.current += TICK_MS / 1000;
|
|
const ratio = Math.min(elapsedRef.current / durationS, 1);
|
|
onProgress(ratio);
|
|
if (ratio >= 1) {
|
|
clearInterval(interval);
|
|
onEnded();
|
|
}
|
|
}, TICK_MS);
|
|
return () => clearInterval(interval);
|
|
}, [paused, durationS, onEnded, onProgress, particle.id]);
|
|
|
|
return (
|
|
<View
|
|
className="flex-1 items-center justify-center px-6"
|
|
style={{ paddingTop: safe.top + 16, paddingBottom: safe.bottom + 16 }}
|
|
>
|
|
<ScrollView
|
|
className="max-h-full w-full max-w-xl rounded-2xl bg-white/10"
|
|
contentContainerClassName="px-5 py-5"
|
|
showsVerticalScrollIndicator
|
|
indicatorStyle="white"
|
|
>
|
|
<Text className="text-white text-2xl font-semibold mb-3">{title}</Text>
|
|
<MarkdownBody content={content} />
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|