step 2.5: streams list
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
@@ -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)];
|
||||
}
|
||||
@@ -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
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user