mobile v0.1 with deployment for ios (#191)
* stage 1: project init * stage 2: skeleton with navigation * step 2.5: streams list * step 4: stream playback experience * step 5-6: compose experience * fix: broken record * transcode media particles to mp4 * build: reproducible go generate * build: rename skaffold module for particle processor worker * infra: increase particle processor worker resources Was dealing with OOM errors * tweaks to mobile * log transcode work * view on desktop placeholder * tweak padding * cap video resolution to save on memory * infra: bump memory limits as insurance * ux improvements * update bundle id for mobile * config for mobile
This commit was merged in pull request #191.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { ScrollView, Text, View } from "react-native";
|
||||
import type { Particle } from "@/api/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useStreamSafeArea } from "./stream-safe-area";
|
||||
|
||||
type TextParticle = Extract<Particle, { type: "text" }>;
|
||||
|
||||
interface TextParticleViewProps {
|
||||
particle: TextParticle;
|
||||
paused: boolean;
|
||||
onEnded: () => void;
|
||||
onProgress: (ratio: number) => void;
|
||||
}
|
||||
|
||||
// Mirrors desktop's read-duration math (chars/min ≈ 1000, plus +2s per
|
||||
// link/attachment, clamped 3–15s). Mobile v1 has no attachments and we
|
||||
// don't extract link previews mid-render, so the formula collapses to
|
||||
// a length-only base.
|
||||
const CHARS_PER_MINUTE = 1000;
|
||||
const MIN_DURATION_S = 3;
|
||||
const MAX_DURATION_S = 15;
|
||||
const TICK_MS = 100;
|
||||
const IMMERSIVE_CHAR_LIMIT = 120;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function getImmersiveStyle(length: number) {
|
||||
if (length < 30)
|
||||
return { className: "text-5xl font-semibold leading-tight" };
|
||||
if (length < 70)
|
||||
return { className: "text-3xl font-semibold leading-snug" };
|
||||
return { className: "text-2xl font-normal leading-snug" };
|
||||
}
|
||||
|
||||
export function TextParticleView({
|
||||
particle,
|
||||
paused,
|
||||
onEnded,
|
||||
onProgress,
|
||||
}: TextParticleViewProps) {
|
||||
const content = particle.properties.content;
|
||||
const durationS = computeReadDuration(content);
|
||||
const elapsedRef = useRef(0);
|
||||
const safe = useStreamSafeArea();
|
||||
|
||||
// Reset when the particle changes.
|
||||
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]);
|
||||
|
||||
// Immersive (short, plain): centered, large type — feels like a lock-screen note.
|
||||
if (content.length < IMMERSIVE_CHAR_LIMIT) {
|
||||
const style = getImmersiveStyle(content.length);
|
||||
return (
|
||||
<View
|
||||
className="flex-1 items-center justify-center px-8"
|
||||
style={{
|
||||
paddingTop: safe.top + 16,
|
||||
paddingBottom: safe.bottom + 16,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={cn("text-white text-center max-w-xl", style.className)}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Long text: scrollable card so the reader can pace themselves; the
|
||||
// duration timer keeps ticking either way, which is intentional —
|
||||
// long messages should still auto-advance at the 15s cap. Padding is
|
||||
// pulled from the StreamSafeArea so the card never slips under chrome.
|
||||
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-lg leading-relaxed">{content}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user