Files
llink/js/mobile/src/features/stream-view/ReactionStack.tsx
T
Arjun Patel e3461dd5cd 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
2026-04-29 17:39:11 -07:00

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>
);
}