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:
@@ -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") {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user