e3461dd5cd
* 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
125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Pressable, Text, View } from "react-native";
|
|
import { Plus } from "lucide-react-native";
|
|
import * as Haptics from "expo-haptics";
|
|
import { REACTION_EMOJIS, type Reactions } from "@/api/types";
|
|
import type { Human } from "@/api/types";
|
|
import { resolveHumanDisplay } from "@/lib/humans";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const EMOJI_SET = new Set<string>(REACTION_EMOJIS);
|
|
|
|
interface ReactionStackProps {
|
|
reactions: Reactions;
|
|
currentHumanId: string;
|
|
humans: Human[] | undefined;
|
|
/** Toggle a reaction (emoji or text) — same contract as ReactionSheet's onToggle. */
|
|
onToggle: (key: string) => void;
|
|
/** Open the full reaction sheet for emoji + custom-text picking. */
|
|
onOpenSheet: () => void;
|
|
}
|
|
|
|
/**
|
|
* Right-edge reaction stack — mobile counterpart of desktop's ReactionBar.
|
|
* Sits vertically centered on the right side of the canvas so the user can
|
|
* see existing reactions at a glance and tap to toggle their own. The "+"
|
|
* affordance opens the ReactionSheet for the full picker (emoji or text).
|
|
*/
|
|
export function ReactionStack({
|
|
reactions,
|
|
currentHumanId,
|
|
humans,
|
|
onToggle,
|
|
onOpenSheet,
|
|
}: ReactionStackProps) {
|
|
const activeEmojis = REACTION_EMOJIS.filter(
|
|
(emoji) => reactions?.[emoji] && (reactions[emoji]?.length ?? 0) > 0,
|
|
);
|
|
const activeTextKeys = useMemo(
|
|
() =>
|
|
Object.keys(reactions ?? {}).filter(
|
|
(k) => !EMOJI_SET.has(k) && (reactions?.[k]?.length ?? 0) > 0,
|
|
),
|
|
[reactions],
|
|
);
|
|
|
|
const handleToggle = (key: string) => {
|
|
void Haptics.selectionAsync();
|
|
onToggle(key);
|
|
};
|
|
|
|
return (
|
|
<View className="items-end gap-1.5">
|
|
{activeEmojis.map((emoji) => {
|
|
const reactors = reactions?.[emoji] ?? [];
|
|
const isMine = reactors.includes(currentHumanId);
|
|
return (
|
|
<Pressable
|
|
key={emoji}
|
|
onPress={() => handleToggle(emoji)}
|
|
className={cn(
|
|
"flex-row items-center gap-1 rounded-full px-2 py-1",
|
|
isMine ? "bg-white/25" : "bg-black/45",
|
|
)}
|
|
style={
|
|
isMine
|
|
? { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" }
|
|
: undefined
|
|
}
|
|
>
|
|
<Text className="text-sm">{emoji}</Text>
|
|
<Text className="text-white/85 text-xs font-medium">
|
|
{reactors.length}
|
|
</Text>
|
|
</Pressable>
|
|
);
|
|
})}
|
|
|
|
{activeTextKeys.map((text) => {
|
|
const reactors = reactions?.[text] ?? [];
|
|
const isMine = reactors.includes(currentHumanId);
|
|
const firstReactor = resolveHumanDisplay(reactors[0], humans);
|
|
return (
|
|
<Pressable
|
|
key={text}
|
|
onPress={() => handleToggle(text)}
|
|
className={cn(
|
|
"flex-row items-center gap-1.5 rounded-full py-1 pl-1 pr-2.5",
|
|
isMine ? "bg-white/25" : "bg-black/45",
|
|
)}
|
|
style={[
|
|
{ maxWidth: 200 },
|
|
isMine
|
|
? { borderWidth: 1, borderColor: "rgba(255,255,255,0.4)" }
|
|
: null,
|
|
]}
|
|
>
|
|
<View className="bg-white/15 h-5 w-5 items-center justify-center rounded-full">
|
|
<Text className="text-white text-[9px] font-semibold">
|
|
{firstReactor.initials}
|
|
</Text>
|
|
</View>
|
|
<Text
|
|
className="text-white/90 text-xs"
|
|
numberOfLines={1}
|
|
>
|
|
{text}
|
|
</Text>
|
|
{reactors.length > 1 ? (
|
|
<Text className="text-white/60 text-xs">{reactors.length}</Text>
|
|
) : null}
|
|
</Pressable>
|
|
);
|
|
})}
|
|
|
|
<Pressable
|
|
onPress={onOpenSheet}
|
|
accessibilityLabel="Add reaction"
|
|
className="h-8 w-8 items-center justify-center rounded-full bg-black/45"
|
|
>
|
|
<Plus color="rgba(255,255,255,0.85)" size={16} strokeWidth={2} />
|
|
</Pressable>
|
|
</View>
|
|
);
|
|
}
|