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 { useEffect, useRef, useState } from "react";
|
||||||
import { Send } from "lucide-react";
|
import { Send } from "lucide-react";
|
||||||
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
|
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
|
||||||
|
import { sanitizeReactionText } from "@/lib/firestore-particles";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const MAX_LENGTH = 40;
|
const MAX_LENGTH = 40;
|
||||||
@@ -44,7 +45,9 @@ export function TextReactionInput({ open, onSubmit, onClose }: TextReactionInput
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={value}
|
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}
|
onBlur={onClose}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Timestamp,
|
Timestamp,
|
||||||
arrayUnion,
|
arrayUnion,
|
||||||
arrayRemove,
|
arrayRemove,
|
||||||
|
FieldPath,
|
||||||
type DocumentData,
|
type DocumentData,
|
||||||
type FirestoreDataConverter,
|
type FirestoreDataConverter,
|
||||||
type QueryDocumentSnapshot,
|
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(
|
export async function toggleParticleReaction(
|
||||||
docPath: string,
|
docPath: string,
|
||||||
emoji: string,
|
emoji: string,
|
||||||
humanId: string,
|
humanId: string,
|
||||||
currentReactions?: Reactions,
|
currentReactions?: Reactions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const key = sanitizeReactionText(emoji);
|
||||||
|
if (!key) return;
|
||||||
const particleRef = typedDoc(docPath);
|
const particleRef = typedDoc(docPath);
|
||||||
const field = `reactions.${emoji}`;
|
const alreadyReacted = currentReactions?.[key]?.includes(humanId) ?? false;
|
||||||
const alreadyReacted = currentReactions?.[emoji]?.includes(humanId) ?? false;
|
await updateDoc(
|
||||||
await updateDoc(particleRef, {
|
particleRef,
|
||||||
[field]: alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
|
new FieldPath("reactions", key),
|
||||||
updated_at: serverTimestamp(),
|
alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
|
||||||
});
|
"updated_at",
|
||||||
|
serverTimestamp(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import Animated, {
|
|||||||
import { REACTION_EMOJIS, type Reactions } from "@/api/types";
|
import { REACTION_EMOJIS, type Reactions } from "@/api/types";
|
||||||
import type { Human } from "@/api/types";
|
import type { Human } from "@/api/types";
|
||||||
import { resolveHumanDisplay } from "@/lib/humans";
|
import { resolveHumanDisplay } from "@/lib/humans";
|
||||||
|
import { sanitizeReactionText } from "@/lib/firestore-particles";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
|
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">
|
<View className="flex-1 bg-white/10 rounded-full px-4 py-2.5">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={text}
|
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..."
|
placeholder="Send a quick reply..."
|
||||||
placeholderTextColor="rgba(255,255,255,0.4)"
|
placeholderTextColor="rgba(255,255,255,0.4)"
|
||||||
maxLength={TEXT_REACTION_MAX}
|
maxLength={TEXT_REACTION_MAX}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Timestamp,
|
Timestamp,
|
||||||
arrayUnion,
|
arrayUnion,
|
||||||
arrayRemove,
|
arrayRemove,
|
||||||
|
FieldPath,
|
||||||
type DocumentData,
|
type DocumentData,
|
||||||
type FirestoreDataConverter,
|
type FirestoreDataConverter,
|
||||||
type QueryDocumentSnapshot,
|
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(
|
export async function toggleParticleReaction(
|
||||||
docPath: string,
|
docPath: string,
|
||||||
emoji: string,
|
emoji: string,
|
||||||
humanId: string,
|
humanId: string,
|
||||||
currentReactions?: Reactions,
|
currentReactions?: Reactions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const key = sanitizeReactionText(emoji);
|
||||||
|
if (!key) return;
|
||||||
const particleRef = typedDoc(docPath);
|
const particleRef = typedDoc(docPath);
|
||||||
const field = `reactions.${emoji}`;
|
const alreadyReacted = currentReactions?.[key]?.includes(humanId) ?? false;
|
||||||
const alreadyReacted = currentReactions?.[emoji]?.includes(humanId) ?? false;
|
await updateDoc(
|
||||||
await updateDoc(particleRef, {
|
particleRef,
|
||||||
[field]: alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
|
new FieldPath("reactions", key),
|
||||||
updated_at: serverTimestamp(),
|
alreadyReacted ? arrayRemove(humanId) : arrayUnion(humanId),
|
||||||
});
|
"updated_at",
|
||||||
|
serverTimestamp(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user