refactor: update api and client to reference humanIds
This commit is contained in:
+8
-7
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const HumanSchema = z.object({
|
||||
id: z.string().nullable(),
|
||||
created_at: z.coerce.date().nullable(),
|
||||
id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
email: z.string().email(),
|
||||
email_prefix: z.string(),
|
||||
});
|
||||
@@ -101,7 +101,8 @@ export const QuestPropertiesSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
status: z.string().optional(),
|
||||
assigned_to: z.string().email().optional(),
|
||||
// humanId
|
||||
assigned_to: z.string().optional(),
|
||||
});
|
||||
export type QuestProperties = z.infer<typeof QuestPropertiesSchema>;
|
||||
|
||||
@@ -126,15 +127,15 @@ export interface ParticlePropertiesMap {
|
||||
const ParticleBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
created_by_email: z.string().email(),
|
||||
created_by_human_id: z.string(),
|
||||
updated_at: z.coerce.date().optional(),
|
||||
});
|
||||
|
||||
export const ParticleSchema = z.discriminatedUnion("type", [
|
||||
ParticleBaseSchema.extend({
|
||||
type: z.literal("stream"), properties: StreamPropertiesSchema,
|
||||
// e.g. ["human:[email protected]", "human:[email protected]"] - visible only to Aron and John
|
||||
// e.g. ["network:123"] - visible to everyone in the network
|
||||
// e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John
|
||||
// e.g. ["network:xywx"] - visible to everyone in the network
|
||||
visible_to: z.array(z.string()),
|
||||
// Marks human_id to their `playback_position_at`: where they left off in a conversation
|
||||
playback_markers: z.record(z.string(), z.coerce.date()).optional(),
|
||||
@@ -144,7 +145,7 @@ export const ParticleSchema = z.discriminatedUnion("type", [
|
||||
}),
|
||||
ParticleBaseSchema.extend({
|
||||
type: z.literal("folder"), properties: FolderPropertiesSchema,
|
||||
// e.g. ["human:[email protected]", "human:[email protected]"] - visible only to Aron and John
|
||||
// e.g. ["human:human_xxxx", "human:human_yyyy"] - visible only to Aron and John
|
||||
// e.g. ["network:123"] - visible to everyone in the network
|
||||
visible_to: z.array(z.string()),
|
||||
}),
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ComposeOverlay({
|
||||
const [reviewMimeType, setReviewMimeType] = useState<string | null>(null);
|
||||
|
||||
const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
|
||||
const userEmail = useAuthStore((s) => s.user?.email);
|
||||
const userId = useAuthStore((s) => s.user?.id);
|
||||
const createParticle = useCreateParticle();
|
||||
const createStream = useCreateStreamParticle();
|
||||
|
||||
@@ -113,7 +113,7 @@ export function ComposeOverlay({
|
||||
|
||||
const createChildParticle = useCallback(
|
||||
async (path: ParticlePath) => {
|
||||
if (!userEmail) return;
|
||||
if (!userId) return;
|
||||
|
||||
let particleId = '';
|
||||
if (textContent.trim()) {
|
||||
@@ -121,7 +121,7 @@ export function ComposeOverlay({
|
||||
path,
|
||||
type: "text",
|
||||
properties: { content: textContent },
|
||||
createdByEmail: userEmail,
|
||||
createdByHumanId: userId,
|
||||
});
|
||||
} else if (reviewBlob && reviewMimeType) {
|
||||
const { object_id, size_bytes } = await uploadMedia(
|
||||
@@ -138,14 +138,14 @@ export function ComposeOverlay({
|
||||
duration_ms: reviewDurationMs,
|
||||
size_bytes,
|
||||
},
|
||||
createdByEmail: userEmail,
|
||||
createdByHumanId: userId,
|
||||
});
|
||||
}
|
||||
|
||||
onParticleCreated?.(particleId);
|
||||
},
|
||||
[
|
||||
userEmail,
|
||||
userId,
|
||||
textContent,
|
||||
reviewBlob,
|
||||
reviewMimeType,
|
||||
@@ -158,7 +158,7 @@ export function ComposeOverlay({
|
||||
|
||||
// Reply mode: create particle directly under targetPath
|
||||
const onSubmitReply = useEffectEvent(async () => {
|
||||
if (!targetPath || !userEmail || stepRef.current === "submitting") return;
|
||||
if (!targetPath || !userId || stepRef.current === "submitting") return;
|
||||
setStepSync("submitting");
|
||||
await createChildParticle(targetPath);
|
||||
cancel();
|
||||
@@ -167,7 +167,7 @@ export function ComposeOverlay({
|
||||
// New stream mode: create stream + first child
|
||||
const handleStreamSubmit = useCallback(
|
||||
async (streamName: string, visibleTo: string[]) => {
|
||||
if (!userEmail || stepRef.current === "submitting") return;
|
||||
if (!userId || stepRef.current === "submitting") return;
|
||||
setStepSync("submitting");
|
||||
|
||||
const streamId = await createStream.mutateAsync({
|
||||
@@ -176,7 +176,7 @@ export function ComposeOverlay({
|
||||
name: streamName,
|
||||
status: "open",
|
||||
},
|
||||
createdByEmail: userEmail,
|
||||
createdByHumanId: userId,
|
||||
visibleTo,
|
||||
});
|
||||
|
||||
@@ -185,7 +185,7 @@ export function ComposeOverlay({
|
||||
|
||||
cancel();
|
||||
},
|
||||
[networkId, userEmail, createParticle, createChildParticle, cancel],
|
||||
[networkId, userId, createParticle, createChildParticle, cancel],
|
||||
);
|
||||
|
||||
// --- Keyboard handling ---
|
||||
|
||||
@@ -24,16 +24,16 @@ export function ConfigureStreamStep({
|
||||
|
||||
const [name, setName] = useState(() => generateRandomName());
|
||||
const [everyone, setEveryone] = useState(true);
|
||||
const userEmail = useAuthStore((s) => s.user?.email);
|
||||
const [selectedEmails, setSelectedEmails] = useState<Set<string>>(new Set());
|
||||
const userId = useAuthStore((s) => s.user?.id);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const members = (network?.humans ?? []).filter((h) => h.email !== userEmail);
|
||||
const members = (network?.humans ?? []).filter((h) => h.id !== userId);
|
||||
|
||||
const toggleMember = useCallback((email: string) => {
|
||||
setSelectedEmails((prev) => {
|
||||
const toggleMember = useCallback((id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(email)) next.delete(email);
|
||||
else next.add(email);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
@@ -41,8 +41,8 @@ export function ConfigureStreamStep({
|
||||
const buildVisibleTo = useCallback((): string[] => {
|
||||
if (everyone && networkId) return [`network:${networkId}`];
|
||||
|
||||
return Array.from(removeDuplicates([...selectedEmails, userEmail])).map((e) => `human:${e}`);
|
||||
}, [everyone, networkId, selectedEmails, userEmail]);
|
||||
return Array.from(removeDuplicates([...selectedIds, userId].filter(Boolean) as string[])).map((id) => `human:${id}`);
|
||||
}, [everyone, networkId, selectedIds, userId]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!name.trim() || !networkId) return;
|
||||
@@ -117,16 +117,16 @@ export function ConfigureStreamStep({
|
||||
<ScrollArea className="max-h-48">
|
||||
<div className="space-y-0.5 p-1">
|
||||
{members.map((member, index) => {
|
||||
const isSelected = selectedEmails.has(member.email);
|
||||
const isSelected = selectedIds.has(member.id);
|
||||
const initials = member.email_prefix
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={member.email}
|
||||
key={member.id}
|
||||
role="button"
|
||||
onClick={() => toggleMember(member.email)}
|
||||
onClick={() => toggleMember(member.id)}
|
||||
className={cn(
|
||||
"flex w-full cursor-pointer items-center gap-2.5 rounded px-3 py-1.5 text-left text-sm transition-colors",
|
||||
"text-white/70 hover:bg-white/5",
|
||||
|
||||
@@ -77,7 +77,7 @@ function TopBar() {
|
||||
const { networkId, "*": rest } = useParams();
|
||||
const segments = [networkId, ...rest?.split("/") ?? []].filter(Boolean);
|
||||
|
||||
const path = rest ? particlePath(networkId!, rest.split("/").filter(Boolean)) : undefined;
|
||||
const path = rest && networkId ? particlePath(networkId, rest.split("/").filter(Boolean)) : undefined;
|
||||
|
||||
const { data: particle } = useParticle(path);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDownloadUrl } from "@/hooks/use-download-url";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Small } from "@/components/ui/typography";
|
||||
import { getInitials } from "@/lib/utils";
|
||||
import { useNetwork } from "@/hooks/use-networks";
|
||||
|
||||
interface AutoplayOverlayProps {
|
||||
networkId: string;
|
||||
@@ -17,12 +18,14 @@ export function AutoplayOverlay({ networkId }: AutoplayOverlayProps) {
|
||||
const { data: url } = useDownloadUrl(activeParticle?.properties.object_id);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const network = useNetwork(networkId);
|
||||
|
||||
if (!activeParticle || !url) return null;
|
||||
|
||||
const isVideo = activeParticle.properties.mime_type?.startsWith("video/");
|
||||
const senderEmail = activeParticle.created_by_email;
|
||||
const senderInitials = getInitials(senderEmail);
|
||||
const senderName = senderEmail.split("@")[0];
|
||||
const creator = network?.humans?.find((h) => h.id === activeParticle.created_by_human_id);
|
||||
const senderInitials = creator ? getInitials(creator.email) : activeParticle.created_by_human_id.slice(0, 2).toUpperCase();
|
||||
const senderName = creator?.email_prefix ?? activeParticle.created_by_human_id;
|
||||
|
||||
const handleClick = () => {
|
||||
stop();
|
||||
|
||||
@@ -51,7 +51,7 @@ export function FallbackParticleView({ particle }: FallbackParticleViewProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
From {particle.created_by_email}
|
||||
From {particle.created_by_human_id}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -30,6 +30,7 @@ import type { Particle, StreamProperties } from "@/api/types";
|
||||
import { useAutoplayStore } from "@/stores/autoplay-store";
|
||||
import { where, Timestamp } from "firebase/firestore";
|
||||
import { RECENCY_WINDOW_HOURS } from "@/lib/constants";
|
||||
import { useNetwork } from "@/hooks/use-networks";
|
||||
|
||||
function getParticleTypeIcon(particle: Particle): LucideIcon {
|
||||
switch (particle.type) {
|
||||
@@ -88,7 +89,7 @@ function StreamRow({
|
||||
const { latestChild } = useLiveLatestChild(streamPath);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const userId = user?.id ?? "";
|
||||
const userEmail = user?.email ?? "";
|
||||
const network = useNetwork(networkId);
|
||||
|
||||
// Autoplay: trigger only when latestChild *changes* to a new media particle,
|
||||
// not on initial data load. We track the "settled" id — the first non-null value
|
||||
@@ -106,7 +107,7 @@ function StreamRow({
|
||||
if (latestChild.id === settledIdRef.current) return;
|
||||
settledIdRef.current = latestChild.id;
|
||||
|
||||
if (latestChild.created_by_email === userEmail) return;
|
||||
if (latestChild.created_by_human_id === userId) return;
|
||||
|
||||
if (latestChild.type === "text") {
|
||||
new Audio(beepSound).play().catch(() => {});
|
||||
@@ -125,20 +126,22 @@ function StreamRow({
|
||||
const initials = useMemo(() => {
|
||||
if (isDM) {
|
||||
const otherEntry = particle.visible_to.find(
|
||||
(v) => v !== `human:${userEmail}`,
|
||||
(v) => v !== `human:${userId}`,
|
||||
);
|
||||
if (otherEntry) {
|
||||
const otherEmail = otherEntry.replace("human:", "");
|
||||
return getInitials(otherEmail);
|
||||
const otherId = otherEntry.replace("human:", "");
|
||||
const otherHuman = network?.humans?.find((h) => h.id === otherId);
|
||||
if (otherHuman) return getInitials(otherHuman.email);
|
||||
}
|
||||
}
|
||||
|
||||
if (latestChild) {
|
||||
return getInitials(latestChild.created_by_email);
|
||||
const creator = network?.humans?.find((h) => h.id === latestChild.created_by_human_id);
|
||||
if (creator) return getInitials(creator.email);
|
||||
}
|
||||
|
||||
return particle.properties.name.slice(0, 2).toUpperCase();
|
||||
}, [isDM, particle.visible_to, particle.properties.name, userEmail, latestChild]);
|
||||
}, [isDM, particle.visible_to, particle.properties.name, userId, latestChild, network]);
|
||||
|
||||
const isUnseen = useMemo(() => {
|
||||
if (!latestChild) return false;
|
||||
@@ -150,17 +153,17 @@ function StreamRow({
|
||||
|
||||
const senderPrefix = useMemo(() => {
|
||||
if (!latestChild) return null;
|
||||
const isCurrentUser = latestChild.created_by_email === userEmail;
|
||||
const isCurrentUser = latestChild.created_by_human_id === userId;
|
||||
if (isDM) {
|
||||
return isCurrentUser ? "You: " : null;
|
||||
}
|
||||
// Group stream
|
||||
if (isCurrentUser) return "You: ";
|
||||
const emailPrefix = latestChild.created_by_email.split("@")[0];
|
||||
const capitalized =
|
||||
emailPrefix.charAt(0).toUpperCase() + emailPrefix.slice(1);
|
||||
const creator = network?.humans?.find((h) => h.id === latestChild.created_by_human_id);
|
||||
const name = creator?.email_prefix ?? latestChild.created_by_human_id;
|
||||
const capitalized = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
return `${capitalized}: `;
|
||||
}, [latestChild, userEmail, isDM]);
|
||||
}, [latestChild, userId, isDM, network]);
|
||||
|
||||
const subtitle = latestChild
|
||||
? getMessagePreview(latestChild)
|
||||
@@ -237,19 +240,19 @@ function StreamRow({
|
||||
|
||||
// Generates the scopes for filtering particles to those that the user has access to
|
||||
function useVisibilityScopes(
|
||||
userEmail?: string,
|
||||
userId?: string,
|
||||
networkId?: string,
|
||||
) {
|
||||
return useMemo(() => {
|
||||
let scopes: string[] = [];
|
||||
if (userEmail) {
|
||||
scopes.push(`human:${userEmail}`);
|
||||
if (userId) {
|
||||
scopes.push(`human:${userId}`);
|
||||
}
|
||||
if (networkId) {
|
||||
scopes.push(`network:${networkId}`);
|
||||
}
|
||||
return scopes;
|
||||
}, [userEmail, networkId]);
|
||||
}, [userId, networkId]);
|
||||
}
|
||||
|
||||
interface ParticleListViewProps {
|
||||
@@ -262,7 +265,7 @@ interface ParticleListViewProps {
|
||||
export function ParticleListView({ path }: ParticleListViewProps) {
|
||||
const { networkId } = parseParticlePath(path);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const visibilityScopes = useVisibilityScopes(user?.email, networkId);
|
||||
const visibilityScopes = useVisibilityScopes(user?.id, networkId);
|
||||
|
||||
const [recencyCutoff, setRecencyCutoff] = useState(() => {
|
||||
const d = new Date();
|
||||
|
||||
@@ -202,7 +202,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
</p>
|
||||
<ControlsIndicator type="reply" />
|
||||
<ComposeOverlay
|
||||
networkId={networkId!}
|
||||
networkId={networkId}
|
||||
targetPath={path}
|
||||
onActiveChange={setComposeActive}
|
||||
/>
|
||||
@@ -267,7 +267,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
||||
</div>
|
||||
|
||||
<ComposeOverlay
|
||||
networkId={networkId!}
|
||||
networkId={networkId}
|
||||
targetPath={path}
|
||||
onActiveChange={setComposeActive}
|
||||
onParticleCreated={onLocalParticleCreated}
|
||||
@@ -333,7 +333,7 @@ function TopBar({ networkId, particle, streamParticle }: { networkId: string; pa
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem className="text-xs">
|
||||
<BreadcrumbPage><ParticleBreadcrumbContent particle={particle} /></BreadcrumbPage>
|
||||
<BreadcrumbPage><ParticleBreadcrumbContent particle={particle} networkId={networkId} /></BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
)}
|
||||
@@ -370,10 +370,11 @@ function NetworkBreadcrumbContent({ networkId }: { networkId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ParticleBreadcrumbContent({ particle }: { particle: Particle }) {
|
||||
const createdByEmail = particle.created_by_email;
|
||||
const prefix = createdByEmail.split('@')[0];
|
||||
const initials = createdByEmail.slice(0, 2).toUpperCase();
|
||||
function ParticleBreadcrumbContent({ particle, networkId }: { particle: Particle; networkId: string }) {
|
||||
const network = useNetwork(networkId);
|
||||
const creator = network?.humans?.find((h) => h.id === particle.created_by_human_id);
|
||||
const prefix = creator?.email_prefix ?? particle.created_by_human_id;
|
||||
const initials = prefix.slice(0, 2).toUpperCase();
|
||||
|
||||
return (
|
||||
<span className="flex
|
||||
@@ -398,27 +399,27 @@ const SeenIndicator = ({ stream, currentParticle, networkId }: { stream: Particl
|
||||
.filter(([userId, timestamp]) => timestamp.getTime() >= currentParticle.created_at.getTime() && userId !== authedUser?.id)
|
||||
.map(([userId, _]) => userId);
|
||||
|
||||
const seenUserEmails = seenUserIds
|
||||
.map((userId) => network?.humans?.find((h) => h.id === userId)?.email)
|
||||
.filter((email): email is string => !!email && email !== currentParticle.created_by_email);
|
||||
const seenHumans = seenUserIds
|
||||
.map((userId) => network?.humans?.find((h) => h.id === userId))
|
||||
.filter((h): h is NonNullable<typeof h> => !!h && h.id !== currentParticle.created_by_human_id);
|
||||
|
||||
if (seenUserIds.length === 0) return null;
|
||||
if (seenHumans.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{seenUserEmails.length > 0 && "Seen by"}
|
||||
{"Seen by"}
|
||||
<AvatarGroup>
|
||||
{seenUserEmails.map((email) => (
|
||||
<Tooltip key={email}>
|
||||
{seenHumans.map((human) => (
|
||||
<Tooltip key={human.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar size="sm">
|
||||
<AvatarFallback>
|
||||
{email.split("@")[0].slice(0, 2)}
|
||||
{human.email_prefix.slice(0, 2)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Seen by {email.split("@")[0]}</p>
|
||||
<p>Seen by {human.email_prefix}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface CreateParticleParams<T extends ParticleType = ParticleType> {
|
||||
path: ParticlePath;
|
||||
type: T;
|
||||
properties: ParticlePropertiesMap[T];
|
||||
createdByEmail: string;
|
||||
createdByHumanId: string;
|
||||
}
|
||||
|
||||
export function useCreateParticle() {
|
||||
@@ -19,7 +19,7 @@ export function useCreateParticle() {
|
||||
collectionPath,
|
||||
params.type,
|
||||
params.properties,
|
||||
params.createdByEmail,
|
||||
params.createdByHumanId,
|
||||
);
|
||||
|
||||
const streamDocPath = toFirestoreDocPath(params.path);
|
||||
@@ -32,7 +32,7 @@ export function useCreateParticle() {
|
||||
type CreateStreamParticleParams = {
|
||||
networkId: string;
|
||||
properties: ParticlePropertiesMap["stream"];
|
||||
createdByEmail: string;
|
||||
createdByHumanId: string;
|
||||
visibleTo?: string[];
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export function useCreateStreamParticle() {
|
||||
networkCollectionPath,
|
||||
"stream",
|
||||
params.properties,
|
||||
params.createdByEmail,
|
||||
params.createdByHumanId,
|
||||
params.visibleTo,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const particleConverter: FirestoreDataConverter<Particle> = {
|
||||
type: raw.type,
|
||||
properties: raw.properties,
|
||||
created_at: (raw.created_at as Timestamp).toDate(),
|
||||
created_by_email: raw.created_by_email,
|
||||
created_by_human_id: raw.created_by_human_id,
|
||||
updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined,
|
||||
visible_to: raw.visible_to,
|
||||
playback_markers: raw.playback_markers
|
||||
@@ -68,7 +68,7 @@ const particleConverter: FirestoreDataConverter<Particle> = {
|
||||
type: raw.type,
|
||||
properties: raw.properties,
|
||||
created_at: (raw.created_at as Timestamp).toDate(),
|
||||
created_by_email: raw.created_by_email,
|
||||
created_by_human_id: raw.created_by_human_id,
|
||||
updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined,
|
||||
visible_to: raw.visible_to,
|
||||
});
|
||||
@@ -82,7 +82,7 @@ const particleConverter: FirestoreDataConverter<Particle> = {
|
||||
type: raw.type,
|
||||
properties: raw.properties,
|
||||
created_at: (raw.created_at as Timestamp).toDate(),
|
||||
created_by_email: raw.created_by_email,
|
||||
created_by_human_id: raw.created_by_human_id,
|
||||
updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined,
|
||||
});
|
||||
default:
|
||||
@@ -188,7 +188,7 @@ export async function createParticle<T extends ParticleType>(
|
||||
collectionPath: string,
|
||||
type: T,
|
||||
properties: ParticlePropertiesMap[T],
|
||||
createdByEmail: string,
|
||||
createdByHumanId: string,
|
||||
// Must be passed for container types
|
||||
visibleTo?: string[],
|
||||
): Promise<string> {
|
||||
@@ -203,7 +203,7 @@ export async function createParticle<T extends ParticleType>(
|
||||
type,
|
||||
properties,
|
||||
created_at: new Date(),
|
||||
created_by_email: createdByEmail,
|
||||
created_by_human_id: createdByHumanId,
|
||||
...(visibleTo ? { visible_to: visibleTo } : {}),
|
||||
});
|
||||
const ref = await addDoc(typedCollection(collectionPath), particle);
|
||||
|
||||
Reference in New Issue
Block a user