fix: prevent nesting of formatted text reactions

Usage of map was causing issues if the text reaction had special
characters.

Closes #205
This commit is contained in:
talksik
2026-05-17 16:51:37 -07:00
parent 4b468ed1ae
commit 2492188c50
4 changed files with 48 additions and 14 deletions
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { Send } from "lucide-react";
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
import { sanitizeReactionText } from "@/lib/firestore-particles";
import { cn } from "@/lib/utils";
const MAX_LENGTH = 40;
@@ -44,7 +45,9 @@ export function TextReactionInput({ open, onSubmit, onClose }: TextReactionInput
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value.slice(0, MAX_LENGTH))}
onChange={(e) =>
setValue(sanitizeReactionText(e.target.value).slice(0, MAX_LENGTH))
}
onBlur={onClose}
onKeyDown={(e) => {
if (e.key === "Enter") {
+20 -6
View File
@@ -14,6 +14,7 @@ import {
Timestamp,
arrayUnion,
arrayRemove,
FieldPath,
type DocumentData,
type FirestoreDataConverter,
type QueryDocumentSnapshot,
@@ -399,17 +400,30 @@ export async function updateStreamPlaybackMarker(
});
}
// Strip characters that Firestore's dotted field-path syntax treats specially.
// Using FieldPath bypasses the parser, but we also forbid these in stored keys
// so reaction maps stay portable across any future read path.
const RESERVED_REACTION_CHARS = /[~*/[\]]/g;
export function sanitizeReactionText(text: string): string {
return text.replace(RESERVED_REACTION_CHARS, "");
}
export async function toggleParticleReaction(
docPath: string,
emoji: string,
humanId: string,
currentReactions?: Reactions,
): Promise<void> {
const key = sanitizeReactionText(emoji);
if (!key) return;
const particleRef = typedDoc(docPath);
const field = `reactions.${emoji}`;
const alreadyReacted = currentReactions?.[emoji]?.includes(humanId) ?? false;
await updateDoc(particleRef, {
[field]: alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
updated_at: serverTimestamp(),
});
const alreadyReacted = currentReactions?.[key]?.includes(humanId) ?? false;
await updateDoc(
particleRef,
new FieldPath("reactions", key),
alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
"updated_at",
serverTimestamp(),
);
}
@@ -33,6 +33,7 @@ import Animated, {
import { REACTION_EMOJIS, type Reactions } from "@/api/types";
import type { Human } from "@/api/types";
import { resolveHumanDisplay } from "@/lib/humans";
import { sanitizeReactionText } from "@/lib/firestore-particles";
import { cn } from "@/lib/utils";
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
@@ -327,7 +328,9 @@ export function ReactionSheet({
<View className="flex-1 bg-white/10 rounded-full px-4 py-2.5">
<TextInput
value={text}
onChangeText={(v) => setText(v.slice(0, TEXT_REACTION_MAX))}
onChangeText={(v) =>
setText(sanitizeReactionText(v).slice(0, TEXT_REACTION_MAX))
}
placeholder="Send a quick reply..."
placeholderTextColor="rgba(255,255,255,0.4)"
maxLength={TEXT_REACTION_MAX}
+20 -6
View File
@@ -14,6 +14,7 @@ import {
Timestamp,
arrayUnion,
arrayRemove,
FieldPath,
type DocumentData,
type FirestoreDataConverter,
type QueryDocumentSnapshot,
@@ -419,17 +420,30 @@ export async function updateStreamPlaybackMarker(
});
}
// Strip characters that Firestore's dotted field-path syntax treats specially.
// Using FieldPath bypasses the parser, but we also forbid these in stored keys
// so reaction maps stay portable across any future read path.
const RESERVED_REACTION_CHARS = /[~*/[\]]/g;
export function sanitizeReactionText(text: string): string {
return text.replace(RESERVED_REACTION_CHARS, "");
}
export async function toggleParticleReaction(
docPath: string,
emoji: string,
humanId: string,
currentReactions?: Reactions,
): Promise<void> {
const key = sanitizeReactionText(emoji);
if (!key) return;
const particleRef = typedDoc(docPath);
const field = `reactions.${emoji}`;
const alreadyReacted = currentReactions?.[emoji]?.includes(humanId) ?? false;
await updateDoc(particleRef, {
[field]: alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
updated_at: serverTimestamp(),
});
const alreadyReacted = currentReactions?.[key]?.includes(humanId) ?? false;
await updateDoc(
particleRef,
new FieldPath("reactions", key),
alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
"updated_at",
serverTimestamp(),
);
}