step 2.5: streams list

This commit is contained in:
talksik
2026-04-29 11:24:29 -07:00
parent cd1837c5f1
commit 73cc65c5d1
14 changed files with 1816 additions and 28 deletions
+3
View File
@@ -16,10 +16,12 @@
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
"@tanstack/react-query": "^5.90.21",
"clsx": "^2.1.1",
"expo": "~54.0.0",
"expo-constants": "~18.0.13",
"expo-secure-store": "~15.0.8",
"expo-status-bar": "~3.0.9",
"firebase": "^12.10.0",
"nativewind": "^4.1.23",
"react": "19.1.0",
"react-native": "0.81.5",
@@ -30,6 +32,7 @@
"react-native-svg": "15.12.1",
"react-native-worklets": "0.5.1",
"sonner-native": "^0.21.0",
"tailwind-merge": "^3.5.0",
"zod": "^4.3.6",
"zustand": "^5.0.11"
},
@@ -0,0 +1,24 @@
import { useEffect, useState } from "react";
import { Text, type TextProps } from "react-native";
import { formatDistanceToNow } from "@/lib/time-utils";
const MINUTE_MS = 60_000;
interface RelativeTimestampProps extends Omit<TextProps, "children"> {
date: Date;
}
/**
* Re-renders once per minute so labels like "5m ago" stay accurate without
* any per-card timer wiring at the call site.
*/
export function RelativeTimestamp({ date, ...rest }: RelativeTimestampProps) {
const [, force] = useState(0);
useEffect(() => {
const interval = setInterval(() => force((n) => n + 1), MINUTE_MS);
return () => clearInterval(interval);
}, []);
return <Text {...rest}>{formatDistanceToNow(date)}</Text>;
}
@@ -0,0 +1,156 @@
import { memo, useMemo } from "react";
import { Pressable, Text, View } from "react-native";
import type { Particle, StreamProperties } from "@/api/types";
import { isParticleDeleted } from "@/api/types";
import { RelativeTimestamp } from "@/components/RelativeTimestamp";
import { useLiveLatestChild } from "@/hooks/use-particle";
import { useNetwork } from "@/hooks/use-networks";
import { particlePath } from "@/lib/particle-path";
import { cn, getInitials } from "@/lib/utils";
import { useAuthStore } from "@/stores/auth-store";
interface StreamCardProps {
particle: Particle & { type: "stream"; properties: StreamProperties };
networkId: string;
onPress: () => void;
}
/**
* Mobile counterpart of js/desktop/src/features/particles/stream-card.tsx —
* same data wiring (subscribe to the latest child for unread + initials),
* touch-tuned layout (single row, no preview thumbnail in v1).
*/
export const StreamCard = memo(function StreamCard({
particle,
networkId,
onPress,
}: StreamCardProps) {
const streamPath = particlePath(networkId, [particle.id]);
const { latestChild } = useLiveLatestChild(streamPath);
const userId = useAuthStore((s) => s.user?.id) ?? "";
const network = useNetwork(networkId);
const isDM =
particle.visible_to.length === 2 &&
particle.visible_to.every((v) => v.startsWith("human:"));
const initials = useMemo(() => {
if (isDM) {
const otherEntry = particle.visible_to.find(
(v) => v !== `human:${userId}`,
);
if (otherEntry) {
const otherId = otherEntry.replace("human:", "");
const otherHuman = network?.humans?.find((h) => h.id === otherId);
if (otherHuman) return getInitials(otherHuman.email);
}
}
if (latestChild) {
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,
userId,
latestChild,
network,
]);
const isUnseen = useMemo(() => {
if (!latestChild) return false;
const latestChildTimestamp = latestChild.created_at.getTime();
const userPlaybackPosition =
particle.playback_markers?.[userId]?.getTime() ?? 0;
return latestChildTimestamp > userPlaybackPosition;
}, [latestChild, particle.playback_markers, userId]);
const previewLabel = useMemo(() => {
if (!latestChild) return "No messages yet";
if (isParticleDeleted(latestChild)) return "Message deleted";
switch (latestChild.type) {
case "media":
return latestChild.properties.mime_type.startsWith("audio/")
? "Voice message"
: "Video message";
case "text":
return latestChild.properties.content;
case "file":
return latestChild.properties.filename;
case "quest":
return latestChild.properties.title;
case "paper":
return latestChild.properties.title;
default:
return "Update";
}
}, [latestChild]);
return (
<Pressable
onPress={onPress}
android_ripple={{ color: "rgba(0,0,0,0.05)" }}
className={cn(
"bg-card rounded-xl border px-3.5 py-3 flex-row items-center gap-3 active:bg-accent",
isUnseen ? "border-primary" : "border-border",
)}
>
<View
className={cn(
"h-10 w-10 items-center justify-center rounded-full",
isUnseen ? "bg-primary" : "bg-muted",
)}
>
<Text
className={cn(
"text-xs font-semibold",
isUnseen ? "text-primary-foreground" : "text-muted-foreground",
)}
>
{initials}
</Text>
</View>
<View className="flex-1">
<Text
numberOfLines={1}
className={cn(
"text-base",
isUnseen
? "text-foreground font-semibold"
: "text-foreground font-medium",
)}
>
{particle.properties.name}
</Text>
<Text
numberOfLines={1}
className="text-muted-foreground mt-0.5 text-sm"
>
{previewLabel}
</Text>
</View>
<View className="items-end gap-1">
{latestChild ? (
<RelativeTimestamp
date={latestChild.created_at}
className={cn(
"text-xs",
isUnseen ? "text-primary" : "text-muted-foreground",
)}
/>
) : null}
{isUnseen ? (
<View className="bg-primary h-2 w-2 rounded-full" />
) : null}
</View>
</Pressable>
);
});
@@ -1,34 +1,157 @@
import { Pressable, Text, View } from "react-native";
import {
ActivityIndicator,
FlatList,
Pressable,
RefreshControl,
Text,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNetwork } from "@/hooks/use-networks";
import { toast } from "sonner-native";
import { toUserMessage } from "@/lib/errors";
import { particlePath } from "@/lib/particle-path";
import { useNetwork, useNetworks } from "@/hooks/use-networks";
import { useStreamParticles } from "@/hooks/use-stream-particles";
import type { RootStackScreenProps } from "@/navigation/types";
import { StreamCard } from "./StreamCard";
export function StreamListScreen({
route,
navigation,
}: RootStackScreenProps<"StreamList">) {
const network = useNetwork(route.params.networkId);
const { networkId } = route.params;
const network = useNetwork(networkId);
// Re-fetching the network list is the closest stand-in for a hard refresh —
// Firestore subscriptions are already realtime, so pull-to-refresh mainly
// reassures the user and re-tries network metadata.
const { refetch: refetchNetworks, isRefetching: isRefetchingNetworks } =
useNetworks();
const path = particlePath(networkId, []);
// v1: only show open streams. Closed streams are reachable on desktop —
// an "Archived" surface is a follow-up (PRD §20).
const { streams, isLoading, error } = useStreamParticles(path, {
status: "open",
});
return (
<SafeAreaView className="flex-1 bg-background" edges={["top"]}>
<View className="flex-row items-center px-3 py-3 border-b border-border">
<Pressable onPress={() => navigation.goBack()} className="px-2 py-1">
<Text className="text-foreground text-2xl"></Text>
</Pressable>
<Text
className="flex-1 text-center text-foreground text-base font-semibold"
numberOfLines={1}
>
{network?.name ?? "Streams"}
</Text>
<View className="w-8" />
</View>
<Header
title={network?.name ?? "Streams"}
onBack={() => navigation.goBack()}
/>
<View className="flex-1 items-center justify-center px-6">
<Text className="text-muted-foreground text-center">
Stream list coming in step 3.
</Text>
</View>
{error ? (
<ErrorState message={toUserMessage(error)} />
) : isLoading && streams.length === 0 ? (
<LoadingState />
) : streams.length === 0 ? (
<EmptyState />
) : (
<FlatList
data={streams}
keyExtractor={(s) => s.id}
contentContainerClassName="p-3 gap-2 pb-24"
refreshControl={
<RefreshControl
refreshing={isRefetchingNetworks}
onRefresh={() => refetchNetworks()}
/>
}
renderItem={({ item }) => (
<StreamCard
particle={item}
networkId={networkId}
onPress={() =>
navigation.navigate("StreamView", {
networkId,
streamId: item.id,
})
}
/>
)}
/>
)}
<ComposeFab
onPress={() => {
// Real compose flow lands in step 5.
toast("New stream — coming soon");
}}
/>
</SafeAreaView>
);
}
function Header({
title,
onBack,
}: {
title: string;
onBack: () => void;
}) {
return (
<View className="flex-row items-center px-3 py-3 border-b border-border">
<Pressable
onPress={onBack}
className="px-2 py-1"
accessibilityLabel="Back"
>
<Text className="text-foreground text-2xl"></Text>
</Pressable>
<Text
className="flex-1 text-center text-foreground text-base font-semibold"
numberOfLines={1}
>
{title}
</Text>
<View className="w-8" />
</View>
);
}
function LoadingState() {
return (
<View className="flex-1 items-center justify-center">
<ActivityIndicator />
</View>
);
}
function EmptyState() {
return (
<View className="flex-1 items-center justify-center px-6">
<Text className="text-foreground text-lg font-medium text-center">
No streams yet.
</Text>
<Text className="text-muted-foreground mt-2 text-center">
Tap the button below to start one voice, video, or text.
</Text>
</View>
);
}
function ErrorState({ message }: { message: string }) {
return (
<View className="flex-1 items-center justify-center px-6">
<Text className="text-destructive text-center">{message}</Text>
<Text className="text-muted-foreground mt-2 text-center text-xs">
Streams reconnect automatically once the network is back.
</Text>
</View>
);
}
function ComposeFab({ onPress }: { onPress: () => void }) {
return (
<View className="absolute bottom-6 right-6">
<Pressable
onPress={onPress}
className="bg-primary h-14 w-14 items-center justify-center rounded-full active:opacity-80"
accessibilityLabel="New stream"
>
<Text className="text-primary-foreground text-3xl leading-none">+</Text>
</Pressable>
</View>
);
}
+30
View File
@@ -0,0 +1,30 @@
import { initializeApp } from "firebase/app";
import {
initializeAuth,
// `getReactNativePersistence` is documented Firebase RN setup but Firebase
// intentionally omits it from `firebase/auth`'s public type bundle (it
// would pollute web autocomplete). The runtime export exists on every
// platform; this is the workaround the Firebase docs themselves use.
// @ts-expect-error — RN-only symbol missing from public Firebase types.
getReactNativePersistence,
} from "firebase/auth";
import { initializeFirestore } from "firebase/firestore";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { appConfig } from "@/config/env";
export const firebaseApp = initializeApp(appConfig.firebase);
// AsyncStorage persists the Firebase auth token across cold starts. Without
// it, the user would have to re-sign-in to Firestore every launch even though
// the Orion bearer token is in SecureStore.
export const firebaseAuth = initializeAuth(firebaseApp, {
persistence: getReactNativePersistence(AsyncStorage),
});
// Firestore's default WebChannel transport often fails on cellular networks
// and behind aggressive proxies on iOS. Long-polling is the documented
// remedy for React Native and is also what the Firebase team recommends
// for mobile apps using the JS SDK.
export const firestoreDb = initializeFirestore(firebaseApp, {
experimentalAutoDetectLongPolling: true,
});
+186
View File
@@ -0,0 +1,186 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import type { QueryFieldFilterConstraint } from "firebase/firestore";
import {
subscribeToParticle,
subscribeToParticleChildren,
subscribeToLatestChild,
getParticle,
getParticleChildren,
} from "@/lib/firestore-particles";
import type { Particle } from "@/api/types";
import {
type ParticlePath,
toFirestoreDocPath,
toFirestoreChildrenPath,
} from "@/lib/particle-path";
import { logError } from "@/lib/errors";
interface UseLiveParticleResult {
particle: Particle | null;
isLoading: boolean;
error: Error | null;
}
export function useLiveParticle(path: ParticlePath): UseLiveParticleResult {
const [particle, setParticle] = useState<Particle | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setIsLoading(true);
setError(null);
setParticle(null);
const docPath = toFirestoreDocPath(path);
const unsubscribe = subscribeToParticle(
docPath,
(data) => {
setParticle(data);
setIsLoading(false);
},
(err) => {
setError(err);
setIsLoading(false);
},
);
return unsubscribe;
}, [path]);
return { particle, isLoading, error };
}
interface UseLiveParticleChildrenResult {
children: Particle[];
isLoading: boolean;
error: Error | null;
}
interface UseLiveParticleChildrenParams {
orderByField?: string;
orderDirection?: "asc" | "desc";
visibilityScopes?: string[];
onAdded?: (child: Particle) => void;
onRemoved?: (child: Particle, updatedChildren: Particle[]) => void;
whereFilter?: QueryFieldFilterConstraint;
/** Optional cap on results. Changes trigger a re-subscription. */
limit?: number;
}
export function useLiveParticleChildren(
path: ParticlePath | undefined,
{
orderByField = "created_at",
orderDirection = "desc",
visibilityScopes,
onAdded,
onRemoved,
whereFilter,
limit,
}: UseLiveParticleChildrenParams = {},
): UseLiveParticleChildrenResult {
const [children, setChildren] = useState<Particle[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!path) {
setChildren([]);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(null);
setChildren([]);
const collectionPath = toFirestoreChildrenPath(path);
const unsubscribe = subscribeToParticleChildren(collectionPath, {
onData: (data) => {
setChildren(data);
setIsLoading(false);
},
onError: (err) => {
logError(err, { scope: "firestore.particle-children", path });
setError(err);
setIsLoading(false);
},
visibilityScopes,
orderByField,
orderDirection,
onAdded,
onRemoved,
whereFilter,
limit,
});
return unsubscribe;
// The hook intentionally keys only on path/whereFilter/limit — desktop
// does the same. Visibility scope changes are absorbed by the active
// listener; reordering causes a re-subscription.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path, whereFilter, limit]);
return { children, isLoading, error };
}
interface UseLiveLatestChildResult {
latestChild: Particle | null;
isLoading: boolean;
}
export function useLiveLatestChild(
path: ParticlePath,
): UseLiveLatestChildResult {
const [latestChild, setLatestChild] = useState<Particle | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
setLatestChild(null);
const unsubscribe = subscribeToLatestChild(
toFirestoreChildrenPath(path),
(data) => {
setLatestChild(data);
setIsLoading(false);
},
(err) => {
logError(err, { scope: "firestore.latest-child", path });
setIsLoading(false);
},
);
return unsubscribe;
}, [path]);
return { latestChild, isLoading };
}
export function useParticle(path?: ParticlePath) {
return useQuery({
queryKey: ["particle", path],
queryFn: async () => {
if (!path) return null;
const docPath = toFirestoreDocPath(path);
const particle = await getParticle(docPath);
return particle;
},
enabled: !!path,
});
}
export function useParticleChildren(path?: ParticlePath) {
return useQuery({
queryKey: ["particle-children", path],
queryFn: async () => {
if (!path) return [];
const collectionPath = toFirestoreChildrenPath(path);
return getParticleChildren(collectionPath);
},
enabled: !!path,
staleTime: 1000 * 60 * 5, // 5 min — attachments don't change
});
}
@@ -0,0 +1,95 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { where, type QueryFieldFilterConstraint } from "firebase/firestore";
import { useLiveParticleChildren } from "@/hooks/use-particle";
import { useAuthStore } from "@/stores/auth-store";
import { parseParticlePath, type ParticlePath } from "@/lib/particle-path";
import type { Particle, StreamProperties } from "@/api/types";
export type StreamParticle = Particle & {
type: "stream";
properties: StreamProperties;
};
const CLOSED_INITIAL_PAGE_SIZE = 50;
const CLOSED_PAGE_INCREMENT = 50;
// Stable where-constraint references so the Firestore subscription only
// re-attaches when the tab actually changes, not on every render.
const OPEN_STATUS_FILTER = where("status", "==", "open");
const CLOSED_STATUS_FILTER = where("status", "==", "closed");
function useVisibilityScopes(userId?: string, networkId?: string) {
return useMemo(() => {
const scopes: string[] = [];
if (userId) scopes.push(`human:${userId}`);
if (networkId) scopes.push(`network:${networkId}`);
return scopes;
}, [userId, networkId]);
}
interface UseStreamParticlesOptions {
/**
* Which streams to subscribe to. Open streams are loaded in full (bounded
* by active work — full realtime coverage is needed for autoplay/huddles).
* Closed streams are paginated via `loadMore`.
*/
status: "open" | "closed";
}
interface UseStreamParticlesResult {
streams: StreamParticle[];
isLoading: boolean;
error: Error | null;
networkId: string;
/** True when more closed streams may exist beyond the current window. */
canLoadMore: boolean;
/** Extend the pagination window. No-op on the open tab. */
loadMore: () => void;
}
export function useStreamParticles(
path: ParticlePath,
{ status }: UseStreamParticlesOptions,
): UseStreamParticlesResult {
const { networkId } = parseParticlePath(path);
const user = useAuthStore((s) => s.user);
const visibilityScopes = useVisibilityScopes(user?.id, networkId);
const [closedLimit, setClosedLimit] = useState(CLOSED_INITIAL_PAGE_SIZE);
// Every time the user switches back to the closed tab, start with a fresh
// window. Avoids an ever-growing subscription across a long session.
useEffect(() => {
if (status === "closed") {
setClosedLimit(CLOSED_INITIAL_PAGE_SIZE);
}
}, [status]);
const whereFilter: QueryFieldFilterConstraint =
status === "open" ? OPEN_STATUS_FILTER : CLOSED_STATUS_FILTER;
const limit = status === "closed" ? closedLimit : undefined;
const { children, isLoading, error } = useLiveParticleChildren(path, {
orderByField: "last_child_created_at",
orderDirection: "desc",
visibilityScopes,
whereFilter,
limit,
});
const streams = useMemo(
() => children.filter((c): c is StreamParticle => c.type === "stream"),
[children],
);
// Heuristic: if we got back as many items as we asked for, assume there
// might be more. Clicking load-more when there are no more is a no-op.
const canLoadMore = status === "closed" && streams.length >= closedLimit;
const loadMore = useCallback(() => {
if (status !== "closed") return;
setClosedLimit((prev) => prev + CLOSED_PAGE_INCREMENT);
}, [status]);
return { streams, isLoading, error, networkId, canLoadMore, loadMore };
}
+435
View File
@@ -0,0 +1,435 @@
import {
collection,
doc,
onSnapshot,
addDoc,
getDoc,
getDocs,
updateDoc,
query,
orderBy,
limit,
serverTimestamp,
where,
Timestamp,
arrayUnion,
arrayRemove,
type DocumentData,
type FirestoreDataConverter,
type QueryDocumentSnapshot,
type SnapshotOptions,
type Unsubscribe,
type QueryFieldFilterConstraint,
} from "firebase/firestore";
import { firestoreDb } from "@/firebase";
import { isContainerType, ParticleSchema } from "@/api/types";
import type {
Particle,
ParticleType,
ParticlePropertiesMap,
Reactions,
} from "@/api/types";
// --- Converter ---
const particleConverter: FirestoreDataConverter<Particle> = {
toFirestore(particle: Particle): DocumentData {
const { id: _id, created_at, updated_at, ...rest } = particle;
const deletedAt =
"deleted_at" in particle ? particle.deleted_at : undefined;
return {
...rest,
created_at: Timestamp.fromDate(created_at),
...(updated_at && { updated_at: Timestamp.fromDate(updated_at) }),
...(deletedAt && { deleted_at: Timestamp.fromDate(deletedAt) }),
};
},
fromFirestore(
snap: QueryDocumentSnapshot,
options?: SnapshotOptions,
): Particle {
const raw = snap.data(options);
if (typeof raw.type !== "string") {
throw new Error(`Invalid particle type: ${raw.type}`);
}
const type = raw.type as ParticleType;
switch (type) {
case "stream":
return ParticleSchema.parse({
id: snap.id,
type: raw.type,
properties: raw.properties,
created_at: (raw.created_at as Timestamp).toDate(),
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
? Object.fromEntries(
Object.entries(raw.playback_markers).map(([key, value]) => [
key,
(value as Timestamp).toDate(),
]),
)
: undefined,
last_child_created_at: raw.last_child_created_at
? (raw.last_child_created_at as Timestamp).toDate()
: undefined,
huddle_active_participants:
raw.huddle_active_participants ?? undefined,
status: raw.status ?? undefined,
});
case "folder":
return ParticleSchema.parse({
id: snap.id,
type: raw.type,
properties: raw.properties,
created_at: (raw.created_at as Timestamp).toDate(),
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,
});
case "media":
case "file":
case "text":
case "quest":
case "paper": {
// Firestore stores timestamps as `Timestamp`; zod expects `Date`. Text
// particles carry `properties.edited_at`, so coerce it if present.
const properties =
type === "text" && raw.properties?.edited_at
? {
...raw.properties,
edited_at: (raw.properties.edited_at as Timestamp).toDate(),
}
: raw.properties;
return ParticleSchema.parse({
id: snap.id,
type: raw.type,
properties,
created_at: (raw.created_at as Timestamp).toDate(),
created_by_human_id: raw.created_by_human_id,
updated_at: raw.updated_at
? (raw.updated_at as Timestamp).toDate()
: undefined,
reactions: raw.reactions ?? undefined,
deleted_at: raw.deleted_at
? (raw.deleted_at as Timestamp).toDate()
: undefined,
deleted_by_human_id: raw.deleted_by_human_id ?? undefined,
});
}
default:
throw new Error(`Unknown particle type: ${type}`);
}
},
};
// --- Typed reference helpers ---
function typedDoc(path: string) {
return doc(firestoreDb, path).withConverter(particleConverter);
}
function typedCollection(path: string) {
return collection(firestoreDb, path).withConverter(particleConverter);
}
// --- Exported operations ---
export function subscribeToParticle(
docPath: string,
onData: (particle: Particle | null) => void,
onError: (error: Error) => void,
): Unsubscribe {
return onSnapshot(
typedDoc(docPath),
(snap) => {
onData(snap.exists() ? snap.data() : null);
},
onError,
);
}
export async function getParticle(docPath: string): Promise<Particle | null> {
const docSnap = await getDoc(typedDoc(docPath));
if (!docSnap.exists()) {
return null;
}
return docSnap.data();
}
export interface GetParticleChildrenOptions {
orderByField: string;
orderDirection: "asc" | "desc";
}
export async function getParticleChildren(
collectionPath: string,
{
orderByField = "created_at",
orderDirection = "asc",
}: GetParticleChildrenOptions = {
orderByField: "created_at",
orderDirection: "asc",
},
): Promise<Particle[]> {
const q = query(
typedCollection(collectionPath),
orderBy(orderByField, orderDirection),
);
const snap = await getDocs(q);
return snap.docs.map((d) => d.data());
}
export interface SubscribeToParticleChildrenOptions {
onData: (children: Particle[]) => void;
onError: (error: Error) => void;
visibilityScopes?: string[];
orderByField?: string;
orderDirection?: "asc" | "desc";
onAdded?: (child: Particle) => void;
onRemoved?: (child: Particle, updatedChildren: Particle[]) => void;
whereFilter?: QueryFieldFilterConstraint;
/** Optional cap on results. Applied after order/where constraints. */
limit?: number;
}
export function subscribeToParticleChildren(
collectionPath: string,
{
onData,
onError,
visibilityScopes = [],
orderByField = "created_at",
orderDirection = "desc",
onAdded,
onRemoved,
whereFilter,
limit: limitValue,
}: SubscribeToParticleChildrenOptions,
): Unsubscribe {
let q = query(
typedCollection(collectionPath),
orderBy(orderByField, orderDirection),
);
if (visibilityScopes.length > 0) {
q = query(q, where("visible_to", "array-contains-any", visibilityScopes));
}
if (whereFilter) {
q = query(q, whereFilter);
}
if (limitValue !== undefined) {
q = query(q, limit(limitValue));
}
return onSnapshot(
q,
(snap) => {
const updatedChildren = snap.docs.map((d) => d.data());
onData(updatedChildren);
if (onAdded || onRemoved) {
for (const change of snap.docChanges()) {
if (change.type === "added" && onAdded) onAdded(change.doc.data());
if (change.type === "removed" && onRemoved)
onRemoved(change.doc.data(), updatedChildren);
}
}
},
onError,
);
}
export function subscribeToLatestChild(
collectionPath: string,
onData: (child: Particle | null) => void,
onError: (error: Error) => void,
): Unsubscribe {
const q = query(
typedCollection(collectionPath),
orderBy("created_at", "desc"),
limit(1),
);
return onSnapshot(
q,
(snap) => {
onData(snap.empty ? null : snap.docs[0].data());
},
onError,
);
}
// This creates a new particle document with the given properties and returns its ID.
export async function createParticle<T extends ParticleType>(
collectionPath: string,
type: T,
properties: ParticlePropertiesMap[T],
createdByHumanId: string,
// Must be passed for container types
visibleTo?: string[],
): Promise<string> {
if (isContainerType(type) && (!visibleTo || visibleTo.length === 0)) {
throw new Error(
`visibleTo is required for container type ${type} and cannot be empty`,
);
}
const particle: Particle = ParticleSchema.parse({
id: "", // ignored by toFirestore, but needed to satisfy the type
type,
properties,
created_at: new Date(),
created_by_human_id: createdByHumanId,
...(visibleTo ? { visible_to: visibleTo } : {}),
});
const ref = await addDoc(typedCollection(collectionPath), particle);
return ref.id;
}
export async function createStreamParticle(
collectionPath: string,
properties: ParticlePropertiesMap["stream"],
createdByHumanId: string,
visibleTo?: string[],
): Promise<string> {
if (!visibleTo || visibleTo.length === 0) {
throw new Error("visibleTo is required for streams and cannot be empty");
}
const particle: Particle = ParticleSchema.parse({
id: "",
type: "stream",
properties,
created_at: new Date(),
created_by_human_id: createdByHumanId,
visible_to: visibleTo,
status: "open",
});
const ref = await addDoc(typedCollection(collectionPath), particle);
return ref.id;
}
// This allows updating properties without overwriting the entire properties object
export async function updateParticleProperties<T extends ParticleType>(
docPath: string,
properties: Partial<ParticlePropertiesMap[T]>,
): Promise<void> {
const particleRef = typedDoc(docPath);
// Take the partial and create a new object with dot notation
// e.g. { title: "New Title" } becomes { "properties.title": "New Title" }
const updatedProperties: Record<string, unknown> = {};
for (const key in properties) {
updatedProperties[`properties.${key}`] = properties[key];
}
await updateDoc(particleRef, {
...updatedProperties,
updated_at: serverTimestamp(),
});
}
// Edits the body of a text particle and stamps `properties.edited_at` so
// readers can see that the message was edited (distinct from `updated_at`,
// which is bumped by any write — visibility, reactions, etc.).
export async function editTextParticleContent(
docPath: string,
content: string,
): Promise<void> {
const particleRef = typedDoc(docPath);
await updateDoc(particleRef, {
"properties.content": content,
"properties.edited_at": serverTimestamp(),
updated_at: serverTimestamp(),
});
}
export async function updateParticleVisibleTo(
docPath: string,
visibleTo: string[],
): Promise<void> {
const particleRef = typedDoc(docPath);
const particle = await getParticle(docPath);
if (!particle) {
throw new Error(`Particle not found at path: ${docPath}`);
}
if (!isContainerType(particle.type)) {
throw new Error(
`Only container particles can have visible_to field. Particle at ${docPath} is of type ${particle.type}`,
);
}
await updateDoc(particleRef, {
visible_to: visibleTo,
updated_at: serverTimestamp(),
});
}
// CAUTION: use the other type safe update functions in most cases
// There is no checking whether this field actually exists on the particle type, so it can lead to inconsistent data if used incorrectly
export async function updateParticle(
docPath: string,
fieldName: string,
value: unknown,
): Promise<void> {
const particleRef = typedDoc(docPath);
await updateDoc(particleRef, {
[fieldName]: value,
updated_at: serverTimestamp(),
});
}
export async function updateStreamStatus(
docPath: string,
status: "open" | "closed",
): Promise<void> {
const particleRef = typedDoc(docPath);
await updateDoc(particleRef, { status, updated_at: serverTimestamp() });
}
/**
* Soft-delete (tombstone) a non-container particle. The Firestore doc stays
* in place so concurrent viewers see the deletion inline rather than being
* bumped to an adjacent particle. Idempotent — re-calling on an already
* tombstoned doc just refreshes the timestamp.
*/
export async function softDeleteParticle(
docPath: string,
humanId: string,
): Promise<void> {
const particleRef = typedDoc(docPath);
await updateDoc(particleRef, {
deleted_at: serverTimestamp(),
deleted_by_human_id: humanId,
updated_at: serverTimestamp(),
});
}
export async function updateStreamPlaybackMarker(
docPath: string,
humanId: string,
playbackPositionAt: Date,
): Promise<void> {
const particleRef = typedDoc(docPath);
const markerField = `playback_markers.${humanId}`;
await updateDoc(particleRef, {
[markerField]: Timestamp.fromDate(playbackPositionAt),
updated_at: serverTimestamp(),
});
}
export async function toggleParticleReaction(
docPath: string,
emoji: string,
humanId: string,
currentReactions?: Reactions,
): Promise<void> {
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(),
});
}
+43
View File
@@ -0,0 +1,43 @@
import type { Human } from "@/api/types";
import { getInitials } from "@/lib/utils";
export const REMOVED_MEMBER_LABEL = "Removed member";
export const REMOVED_MEMBER_INITIALS = "";
export interface HumanDisplay {
/** True when the human was found in the provided list. */
exists: boolean;
/** Short name for inline text (e.g. message sender). */
displayName: string;
/** Full email or fallback label for tooltips. */
email: string;
/** Initials for avatar fallback. */
initials: string;
}
/**
* Resolve a human's display info by id, falling back consistently when the
* human has been removed from the network. Member content (particles, reactions,
* etc.) is retained after removal, so every render path needs a graceful fallback
* instead of leaking raw ids into the UI.
*/
export function resolveHumanDisplay(
humanId: string | null | undefined,
humans: Human[] | undefined,
): HumanDisplay {
const human = humanId ? humans?.find((h) => h.id === humanId) : undefined;
if (!human) {
return {
exists: false,
displayName: REMOVED_MEMBER_LABEL,
email: REMOVED_MEMBER_LABEL,
initials: REMOVED_MEMBER_INITIALS,
};
}
return {
exists: true,
displayName: human.email_prefix,
email: human.email,
initials: getInitials(human.email),
};
}
+70
View File
@@ -0,0 +1,70 @@
/**
* ParticlePath is a branded string type representing a URL-style path
* to a particle in the hierarchy: /{networkId}/{segment1}/{segment2}/...
*
* Using a branded type prevents accidentally passing raw strings where
* a validated particle path is expected.
*/
declare const __brand: unique symbol;
export type ParticlePath = string & { readonly [__brand]: true };
/**
* Construct a ParticlePath from a network ID and optional particle segments.
*
* @example
* particlePath("net1", []) // => "/net1"
* particlePath("net1", ["p1"]) // => "/net1/p1"
* particlePath("net1", ["p1","p2"])// => "/net1/p1/p2"
*/
export function particlePath(
networkId: string,
segments: string[] = [],
): ParticlePath {
return `/${[networkId, ...segments].join("/")}` as ParticlePath;
}
/**
* Parse a ParticlePath back into its network ID and particle segments.
*/
export function parseParticlePath(path: ParticlePath): {
networkId: string;
segments: string[];
} {
const parts = path.split("/").filter(Boolean);
return { networkId: parts[0], segments: parts.slice(1) };
}
/**
* Convert a ParticlePath to the Firestore document path for that particle.
*
* Firestore structure:
* /net1 → networks/net1/children (collection)
* /net1/p1 → networks/net1/children/p1 (document)
* /net1/p1/p2 → networks/net1/children/p1/children/p2 (document)
*/
export function toFirestoreDocPath(path: ParticlePath): string {
const { networkId, segments } = parseParticlePath(path);
const base = `networks/${networkId}/children`;
if (segments.length === 0) return base;
const parts: string[] = [base, segments[0]];
for (let i = 1; i < segments.length; i++) {
parts.push("children", segments[i]);
}
return parts.join("/");
}
/**
* Convert a ParticlePath to the Firestore collection path for its children.
*
* /net1 → networks/net1/children (root particles)
* /net1/p1 → networks/net1/children/p1/children
* /net1/p1/p2 → networks/net1/children/p1/children/p2/children
*/
export function toFirestoreChildrenPath(path: ParticlePath): string {
const { segments } = parseParticlePath(path);
if (segments.length === 0) {
return toFirestoreDocPath(path);
}
return `${toFirestoreDocPath(path)}/children`;
}
+20
View File
@@ -0,0 +1,20 @@
const MINUTE = 60;
const HOUR = 3600;
const DAY = 86400;
const WEEK = 604800;
const MONTH = 2592000;
const YEAR = 31536000;
export function formatDistanceToNow(date: Date | string): string {
const ms = typeof date === "string" ? new Date(date).getTime() : date.getTime();
const seconds = Math.floor((Date.now() - ms) / 1000);
if (seconds < 5) return "just now";
if (seconds < MINUTE) return `${seconds}s ago`;
if (seconds < HOUR) return `${Math.floor(seconds / MINUTE)}m ago`;
if (seconds < DAY) return `${Math.floor(seconds / HOUR)}h ago`;
if (seconds < WEEK) return `${Math.floor(seconds / DAY)}d ago`;
if (seconds < MONTH) return `${Math.floor(seconds / WEEK)}w ago`;
if (seconds < YEAR) return `${Math.floor(seconds / MONTH)}mo ago`;
return `${Math.floor(seconds / YEAR)}y ago`;
}
+15
View File
@@ -0,0 +1,15 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function getInitials(email: string): string {
const prefix = email.split("@")[0] ?? "";
return prefix.slice(0, 2).toUpperCase();
}
export function removeDuplicates<T>(array: T[]): T[] {
return [...new Set(array)];
}
+16 -5
View File
@@ -1,15 +1,23 @@
import { create } from "zustand";
import {
signInWithCustomToken,
signOut as firebaseSignOut,
} from "firebase/auth";
import { apiClient } from "@/api/client";
import type { Human } from "@/api/types";
import { firebaseAuth } from "@/firebase";
import { logError, ApiError } from "@/lib/errors";
import { hydrateSession, useSessionStore } from "./session-store";
// TODO(step-3): once @react-native-firebase/auth lands, mirror desktop's
// signInToFirebase() — fetch /auth/firebase-token then signInWithCustomToken.
// Until then, Firestore reads will fail with permission errors; mobile only
// uses Orion REST until that wiring exists.
async function signInToFirebase(): Promise<void> {
return;
try {
const { token } = await apiClient.getFirebaseToken();
await signInWithCustomToken(firebaseAuth, token);
} catch (err) {
// Firestore subscriptions will fail until the next successful sign-in; the
// rest of the app keeps working against Orion. Sentry catches the failure.
logError(err, { scope: "auth.firebase" });
}
}
type AuthStatus = "idle" | "restoring" | "unauthenticated" | "authenticated";
@@ -98,6 +106,9 @@ export const useAuthStore = create<AuthState>((set) => ({
// Best-effort — sign out locally regardless.
logError(err, { scope: "auth.signOut" });
} finally {
await firebaseSignOut(firebaseAuth).catch((err) =>
logError(err, { scope: "auth.firebaseSignOut" }),
);
await useSessionStore.getState().clearToken();
set({
status: "unauthenticated",
+580 -3
View File
@@ -1115,6 +1115,417 @@
chalk "^4.1.0"
js-yaml "^4.1.0"
"@firebase/ai@2.11.1":
version "2.11.1"
resolved "https://registry.yarnpkg.com/@firebase/ai/-/ai-2.11.1.tgz#a78c8d8a8acc5261fb2e0fa0216209b43a57c6dc"
integrity sha512-WGTF81W3WBKJY+c7xqTzO15OGAkCAs8cpADqflAI0skhTZjIkhF0qyf55rq4Ctt6jKygkv99rPfMrjAHTgXaVQ==
dependencies:
"@firebase/app-check-interop-types" "0.3.3"
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/analytics-compat@0.2.27":
version "0.2.27"
resolved "https://registry.yarnpkg.com/@firebase/analytics-compat/-/analytics-compat-0.2.27.tgz#58ef74a91930267923577f07ca371182d9f7ce2e"
integrity sha512-ZObpYpAxL6JfgH7GnvlDD0sbzGZ0o4nijV8skatV9ZX49hJtCYbFqaEcPYptT94rgX1KUoKEderC7/fa7hybtw==
dependencies:
"@firebase/analytics" "0.10.21"
"@firebase/analytics-types" "0.8.3"
"@firebase/component" "0.7.2"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/analytics-types@0.8.3":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.8.3.tgz#d08cd39a6209693ca2039ba7a81570dfa6c1518f"
integrity sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==
"@firebase/analytics@0.10.21":
version "0.10.21"
resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.10.21.tgz#109d95d287acefe3d8276835291dbbcf4688c18c"
integrity sha512-j2y2q65BlgLGB5Pwjhv/Jopw2X/TBTzvAtI5z/DSp56U4wBj7LfhBfzbdCtFPges+Wz0g55GdoawXibOH5jGng==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/installations" "0.6.21"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/app-check-compat@0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@firebase/app-check-compat/-/app-check-compat-0.4.2.tgz#c0b3808ebe9a366d3b2cba295eb7a1587de66314"
integrity sha512-M91NhxqbSkI0ChkJWy69blC+rPr6HEgaeRllddSaU1pQ/7IiegeCQM9pPDIgvWnwnBSzKhUHpe6ro/jhJ+cvzw==
dependencies:
"@firebase/app-check" "0.11.2"
"@firebase/app-check-types" "0.5.3"
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/app-check-interop-types@0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz#ed9c4a4f48d1395ef378f007476db3940aa5351a"
integrity sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==
"@firebase/app-check-types@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.5.3.tgz#38ba954acf4bffe451581a32fffa20337f11d8e5"
integrity sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==
"@firebase/app-check@0.11.2":
version "0.11.2"
resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.11.2.tgz#c7f771c5222d77a810978081e7b493d3f5e8968f"
integrity sha512-jcXQVMHAQ5AEKzVD5C7s5fmAYeFOuN6lAJeNTgZK2B9aLnofWaJt8u1A8Idm8gpsBBYSaY3cVyeH5SWMOVPBLQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/app-compat@0.5.11":
version "0.5.11"
resolved "https://registry.yarnpkg.com/@firebase/app-compat/-/app-compat-0.5.11.tgz#4cb54f447cb03446465a05d00a71509b5f2ec620"
integrity sha512-KaACDjXkK5VLpI01vEs592R7/8s5DjFdIXfKoR385ly1SmK3Tu+jMHCIB4MsiY5jsez6v7VlEX/3rJ90dVkHyA==
dependencies:
"@firebase/app" "0.14.11"
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/app-types@0.9.4":
version "0.9.4"
resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.9.4.tgz#e85864332b591db95a10620668405bef6906947b"
integrity sha512-crX9TA5SVYZwLPG7/R16IsH8FLlgkPXjJUVhsVpHVDSqJiq3D/NuFTM5ctxGTExXAOeIn//69tQw47CPerM8MQ==
dependencies:
"@firebase/logger" "0.5.0"
"@firebase/app@0.14.11":
version "0.14.11"
resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.14.11.tgz#150f5f98299c24569fab8fbbffb7295075f526d7"
integrity sha512-yxADFW35LYkP8oSGobGsYIrI42I+GPCvKTNHx4meT9Yq3C950IVz1eANoBk822I9tbKv1wyv9P4Bv1G5TpucFw==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
idb "7.1.1"
tslib "^2.1.0"
"@firebase/auth-compat@0.6.5":
version "0.6.5"
resolved "https://registry.yarnpkg.com/@firebase/auth-compat/-/auth-compat-0.6.5.tgz#d12d27a4c2230d3ee32ef5c200ebd3b9108528ca"
integrity sha512-IfVsafZ3QiXbsydXTP/XMI0wVYbJLI1rkb8Qqf03/h5FnL+upbbPOb+6Yj3RpcX+Y1iP5Uh18lxTHlXfbiyAow==
dependencies:
"@firebase/auth" "1.13.0"
"@firebase/auth-types" "0.13.0"
"@firebase/component" "0.7.2"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/auth-interop-types@0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz#176a08686b0685596ff03d7879b7e4115af53de0"
integrity sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==
"@firebase/auth-types@0.13.0":
version "0.13.0"
resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.13.0.tgz#ae6e0015e3bd4bfe18edd0942b48a0a118a098d9"
integrity sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==
"@firebase/auth@1.13.0":
version "1.13.0"
resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-1.13.0.tgz#da83853b465c1ab4b638e542d78df6b3c4855a15"
integrity sha512-mKkSLNym3UbnnZ06dAmtqzp5EpPGCANGCZDJbkoR135aoUdKG6Aizwcnp29RzsQpwH0nmy5nay17Sfbsh9oY8A==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/component@0.7.2":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.7.2.tgz#82a87848ad389d6037019745c973e4f2779a6983"
integrity sha512-iyVDGc6Vjx7Rm0cAdccLH/NG6fADsgJak/XW9IA2lPf8AjIlsemOpFGKczYyPHxm4rnKdR8z6sK4+KEC7NwmEg==
dependencies:
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/data-connect@0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@firebase/data-connect/-/data-connect-0.6.0.tgz#c4581d13685eccac724895b8c7292df4a24cd334"
integrity sha512-OiugPRcdlhqXF97oR9CjVObILmsWU0dFUS0gXNYEe4bDfpW8pZmQ5GqhIPPtLWbT/0W2lMJJD7VILFMk+xuHPg==
dependencies:
"@firebase/auth-interop-types" "0.2.4"
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/database-compat@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@firebase/database-compat/-/database-compat-2.1.3.tgz#19a512209afbba9710d7febc52cd875e1b239e3c"
integrity sha512-GMyfWjD8mehjg/QpNkY/tl9G/MoeugPeg91n9D0atggxbWuKF/2KhVPHZDH+XmoP0EKYqMWYTtKxBsaBaNKLYQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/database" "1.1.2"
"@firebase/database-types" "1.0.19"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/database-types@1.0.19":
version "1.0.19"
resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-1.0.19.tgz#0e59454ea764aa2617fb2a8ea94104c4cb1605ac"
integrity sha512-FqewjUZmV9LqFfuEnmgdcUpiOUz7qwLXxnm/H8BcMFEzQXtd1yyUDm8ex5VRad2nuTE+ahOuCjUAM/cyDncO+g==
dependencies:
"@firebase/app-types" "0.9.4"
"@firebase/util" "1.15.0"
"@firebase/database@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@firebase/database/-/database-1.1.2.tgz#cd99d7f205d7eee121e2c327bf89b42c3afa3776"
integrity sha512-lP96CMjMPy/+d1d9qaaHjHHdzdwvEOuyyLq9ehX89e2XMKwS1jHNzYBO+42bdSumuj5ukPbmnFtViZu8YOMT+w==
dependencies:
"@firebase/app-check-interop-types" "0.3.3"
"@firebase/auth-interop-types" "0.2.4"
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
faye-websocket "0.11.4"
tslib "^2.1.0"
"@firebase/firestore-compat@0.4.8":
version "0.4.8"
resolved "https://registry.yarnpkg.com/@firebase/firestore-compat/-/firestore-compat-0.4.8.tgz#00651c9d01f940d9906b34ae2cc4229527f4b7f8"
integrity sha512-WK9NJRpnosGD2nuyjdr7K+Ht7AxRYJlTF62myI4rRA7ibJOosbecvjacR5oirJ7s1BgNS6qzcBw7n4fD3a5w1w==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/firestore" "4.14.0"
"@firebase/firestore-types" "3.0.3"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/firestore-types@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-3.0.3.tgz#7d0c3dd8850c0193d8f5ee0cc8f11961407742c1"
integrity sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==
"@firebase/firestore@4.14.0":
version "4.14.0"
resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-4.14.0.tgz#a17801fe2e8a0095fc655d38cd791c4a48058cb4"
integrity sha512-bZc6YOjRkMBVA16527tgzi6iN9n//xRB3Mmx/R+Gr6UAP/+xrIKOejQIcn1hh+tCzNT8jO0jI+kWox5J4tB/qQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
"@firebase/webchannel-wrapper" "1.0.5"
"@grpc/grpc-js" "~1.9.0"
"@grpc/proto-loader" "^0.7.8"
tslib "^2.1.0"
"@firebase/functions-compat@0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@firebase/functions-compat/-/functions-compat-0.4.3.tgz#bb7e5b8330143db594466c1140267799cf41680d"
integrity sha512-BxkEwWgx1of0tKaao/r2VR6WBLk/RAiyztatiONPrPE8gkitFkOnOCxf8i9cUyA5hX5RGt5H30uNn25Q6QNEmQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/functions" "0.13.3"
"@firebase/functions-types" "0.6.3"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/functions-types@0.6.3":
version "0.6.3"
resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.6.3.tgz#f5faf770248b13f45d256f614230da6a11bfb654"
integrity sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==
"@firebase/functions@0.13.3":
version "0.13.3"
resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.13.3.tgz#e923cb6f6763531cbe909253155920abb4105f04"
integrity sha512-csO7ckK3SSs+NUZW1nms9EK7ckHe/1QOjiP8uAkCYa7ND18s44vjE9g3KxEeIUpyEPqZaX1EhJuFyZjHigAcYw==
dependencies:
"@firebase/app-check-interop-types" "0.3.3"
"@firebase/auth-interop-types" "0.2.4"
"@firebase/component" "0.7.2"
"@firebase/messaging-interop-types" "0.2.3"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/installations-compat@0.2.21":
version "0.2.21"
resolved "https://registry.yarnpkg.com/@firebase/installations-compat/-/installations-compat-0.2.21.tgz#c00e1e1b3957aff0957cff80a776109895328c54"
integrity sha512-zahIUkaVKbR8zmTeBHkdfaVl6JGWlhVoSjF7CVH33nFqD3SlPEpEEegn2GNT5iAfsVdtlCyJJ9GW4YKjq+RJKQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/installations" "0.6.21"
"@firebase/installations-types" "0.5.3"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/installations-types@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.5.3.tgz#cac8a14dd49f09174da9df8ae453f9b359c3ef2f"
integrity sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==
"@firebase/installations@0.6.21":
version "0.6.21"
resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.6.21.tgz#38c9a5487c7ccc7dd4328736556afad8eebf453e"
integrity sha512-xGFGTeICJZ5vhrmmDukeczIcFULFXybojML2+QSDFoKj5A7zbGN7KzFGSKNhDkIxpjzsYG9IleJyUebuAcmqWA==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/util" "1.15.0"
idb "7.1.1"
tslib "^2.1.0"
"@firebase/logger@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.5.0.tgz#a9e55b1c669a0983dc67127fa4a5964ce8ed5e1b"
integrity sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==
dependencies:
tslib "^2.1.0"
"@firebase/messaging-compat@0.2.25":
version "0.2.25"
resolved "https://registry.yarnpkg.com/@firebase/messaging-compat/-/messaging-compat-0.2.25.tgz#1fd6f317d303dfab57ec51409eb41c89080f2dd6"
integrity sha512-eoOQqGLtRlseTdiemTN44LlHZpltK5gnhq8XVUuLgtIOG+odtDzrz2UoTpcJWSzaJQVxNLb/x9f39tHdDM4N4w==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/messaging" "0.12.25"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/messaging-interop-types@0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz#e647c9cd1beecfe6a6e82018a6eec37555e4da3e"
integrity sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==
"@firebase/messaging@0.12.25":
version "0.12.25"
resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.12.25.tgz#465135006a5d728efaeea1d35790f0e6e20a3e54"
integrity sha512-7RhDwoDHlOK1/ou0/LeubxmjcngsTjDdrY/ssg2vwAVpUuVAhQzQvuCAOYxcX5wNC1zCgQ54AP1vdngBwbCmOQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/installations" "0.6.21"
"@firebase/messaging-interop-types" "0.2.3"
"@firebase/util" "1.15.0"
idb "7.1.1"
tslib "^2.1.0"
"@firebase/performance-compat@0.2.24":
version "0.2.24"
resolved "https://registry.yarnpkg.com/@firebase/performance-compat/-/performance-compat-0.2.24.tgz#1c970640119a8839f7447f624de365f150f21399"
integrity sha512-YRlejH8wLt7ThWao+HXoKUHUrZKGYq+otxkPS+8nuE5PeN1cBXX7NAJl9ueuUkBwMIrnKdnDqL/voHXxDAAt3g==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/performance" "0.7.11"
"@firebase/performance-types" "0.2.3"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/performance-types@0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.2.3.tgz#5ce64e90fa20ab5561f8b62a305010cf9fab86fb"
integrity sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==
"@firebase/performance@0.7.11":
version "0.7.11"
resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.7.11.tgz#238a805f78c5411f1d41c4cdba7854ca4115a1b7"
integrity sha512-V3uAhrz7IYJuji+OgT3qYTGKxpek/TViXti9OSsUJ4AexZ3jQjYH5Yrn7JvBxk8MGiSLsC872hh+BxQiPZsm7g==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/installations" "0.6.21"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
web-vitals "^4.2.4"
"@firebase/remote-config-compat@0.2.23":
version "0.2.23"
resolved "https://registry.yarnpkg.com/@firebase/remote-config-compat/-/remote-config-compat-0.2.23.tgz#3614d83c1f22c68152793bbbf6965715e1716d07"
integrity sha512-4+KqRRHEUUmKT6tFmnpWATOsaFfmSuBs1jXH8JzVtMLEYqq/WS9IDM92OdefFDSrAA2xGd0WN004z8mKeIIscw==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/logger" "0.5.0"
"@firebase/remote-config" "0.8.2"
"@firebase/remote-config-types" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/remote-config-types@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz#f0f503b32edda3384f5252f9900cd9613adbb99c"
integrity sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==
"@firebase/remote-config@0.8.2":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.8.2.tgz#389d2b01d4d877c6cb13bf85692dfda45d61fe6d"
integrity sha512-5EXqOThV4upjK9D38d/qOSVwOqRhemlaOFk9vCkMNNALeIlwr+4pLjtLNo4qoY8etQmU/1q4aIATE9N8PFqg0g==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/installations" "0.6.21"
"@firebase/logger" "0.5.0"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/storage-compat@0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@firebase/storage-compat/-/storage-compat-0.4.2.tgz#866e3bfc4533510bc1d6207d10e9ef5748359cf4"
integrity sha512-R+aB38wxCH5zjIO/xu9KznI7fgiPuZAG98uVm1NcidHyyupGgIDLKigGmRGBZMnxibe/m2oxNKoZpfEbUX2aQQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/storage" "0.14.2"
"@firebase/storage-types" "0.8.3"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/storage-types@0.8.3":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.8.3.tgz#2531ef593a3452fc12c59117195d6485c6632d3d"
integrity sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==
"@firebase/storage@0.14.2":
version "0.14.2"
resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.14.2.tgz#476bad2594ad26487b232620f0a23991d65ab69a"
integrity sha512-o/culaTeJ8GRpKXRJov21rux/n9dRaSOWLebyatFP2sqEdCxQPjVA1H9Z2fzYwQxMIU0JVmC7SPPmU11v7L6vQ==
dependencies:
"@firebase/component" "0.7.2"
"@firebase/util" "1.15.0"
tslib "^2.1.0"
"@firebase/util@1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.15.0.tgz#783c1a67dc0690dbe3afca668174c11368843008"
integrity sha512-AmWf3cHAOMbrCPG4xdPKQaj5iHnyYfyLKZxwz+Xf55bqKbpAmcYifB4jQinT2W9XhDRHISOoPyBOariJpCG6FA==
dependencies:
tslib "^2.1.0"
"@firebase/webchannel-wrapper@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz#39cf5a600450cb42f1f0b507cc385459bf103b27"
integrity sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==
"@grpc/grpc-js@~1.9.0":
version "1.9.15"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.15.tgz#433d7ac19b1754af690ea650ab72190bd700739b"
integrity sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==
dependencies:
"@grpc/proto-loader" "^0.7.8"
"@types/node" ">=12.12.47"
"@grpc/proto-loader@^0.7.8":
version "0.7.15"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60"
integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==
dependencies:
lodash.camelcase "^4.3.0"
long "^5.0.0"
protobufjs "^7.2.5"
yargs "^17.7.2"
"@isaacs/fs-minipass@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32"
@@ -1275,6 +1686,59 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==
"@protobufjs/base64@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
"@protobufjs/codegen@^2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659"
integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==
"@protobufjs/eventemitter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==
"@protobufjs/fetch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==
dependencies:
"@protobufjs/aspromise" "^1.1.1"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/float@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==
"@protobufjs/inquire@^1.1.0", "@protobufjs/inquire@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.1.tgz#6cb936f4ac50965230af1e9d0bbfd57ea3675aa4"
integrity sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==
"@protobufjs/path@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==
"@protobufjs/pool@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==
"@protobufjs/utf8@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774"
integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==
"@react-native-async-storage/async-storage@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz#a3aa565253e46286655560172f4e366e8969f5ad"
@@ -1563,7 +2027,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/node@*":
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
version "25.6.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.6.0.tgz#4e09bad9b469871f2d0f68140198cbd714f4edca"
integrity sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==
@@ -2138,6 +2602,11 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -2649,6 +3118,13 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
faye-websocket@0.11.4:
version "0.11.4"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==
dependencies:
websocket-driver ">=0.5.1"
fb-watchman@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c"
@@ -2694,6 +3170,40 @@ find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
firebase@^12.10.0:
version "12.12.1"
resolved "https://registry.yarnpkg.com/firebase/-/firebase-12.12.1.tgz#4c5145ce819509b1e547d27aef584ab719809d29"
integrity sha512-ee7xA+bTJLfjB9BP/8FQr3EkxmpAAGc1lNc5QkWgTDpUw24HYXFPm7FEWRdLtGnygxIdYpFmepSc5VjkI6NHhw==
dependencies:
"@firebase/ai" "2.11.1"
"@firebase/analytics" "0.10.21"
"@firebase/analytics-compat" "0.2.27"
"@firebase/app" "0.14.11"
"@firebase/app-check" "0.11.2"
"@firebase/app-check-compat" "0.4.2"
"@firebase/app-compat" "0.5.11"
"@firebase/app-types" "0.9.4"
"@firebase/auth" "1.13.0"
"@firebase/auth-compat" "0.6.5"
"@firebase/data-connect" "0.6.0"
"@firebase/database" "1.1.2"
"@firebase/database-compat" "2.1.3"
"@firebase/firestore" "4.14.0"
"@firebase/firestore-compat" "0.4.8"
"@firebase/functions" "0.13.3"
"@firebase/functions-compat" "0.4.3"
"@firebase/installations" "0.6.21"
"@firebase/installations-compat" "0.2.21"
"@firebase/messaging" "0.12.25"
"@firebase/messaging-compat" "0.2.25"
"@firebase/performance" "0.7.11"
"@firebase/performance-compat" "0.2.24"
"@firebase/remote-config" "0.8.2"
"@firebase/remote-config-compat" "0.2.23"
"@firebase/storage" "0.14.2"
"@firebase/storage-compat" "0.4.2"
"@firebase/util" "1.15.0"
flow-enums-runtime@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787"
@@ -2867,6 +3377,11 @@ http-errors@~2.0.1:
statuses "~2.0.2"
toidentifier "~1.0.1"
http-parser-js@>=0.5.1:
version "0.5.10"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075"
integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==
https-proxy-agent@^7.0.5:
version "7.0.6"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
@@ -2875,6 +3390,11 @@ https-proxy-agent@^7.0.5:
agent-base "^7.1.2"
debug "4"
idb@7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -3332,6 +3852,11 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -3349,6 +3874,11 @@ log-symbols@^2.2.0:
dependencies:
chalk "^2.0.1"
long@^5.0.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83"
integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==
loose-envify@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -4268,6 +4798,24 @@ prompts@^2.3.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
protobufjs@^7.2.5:
version "7.5.6"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.6.tgz#11af832ebc4b4326f658a5b1308e6141eb57edfd"
integrity sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.5"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.1"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.1"
"@types/node" ">=13.7.0"
long "^5.0.0"
punycode@^2.1.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
@@ -4600,7 +5148,7 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
safe-buffer@5.2.1:
safe-buffer@5.2.1, safe-buffer@>=5.1.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -4884,6 +5432,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tailwind-merge@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz#06502f4496ba15151445d97d916a26564d50d1ca"
integrity sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==
tailwindcss@^3.4.17:
version "3.4.19"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.19.tgz#af2a0a4ae302d52ebe078b6775e799e132500ee2"
@@ -4999,6 +5552,11 @@ ts-interface-checker@^0.1.9:
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
tslib@^2.1.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
type-detect@4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
@@ -5124,11 +5682,30 @@ wcwidth@^1.0.1:
dependencies:
defaults "^1.0.3"
web-vitals@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7"
integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==
websocket-driver@>=0.5.1:
version "0.7.4"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
dependencies:
http-parser-js ">=0.5.1"
safe-buffer ">=5.1.0"
websocket-extensions ">=0.1.1"
websocket-extensions@>=0.1.1:
version "0.1.4"
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
whatwg-fetch@^3.0.0:
version "3.6.20"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70"
@@ -5245,7 +5822,7 @@ yargs-parser@^21.1.1:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^17.6.2:
yargs@^17.6.2, yargs@^17.7.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==