wip
This commit is contained in:
@@ -5,5 +5,7 @@ import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('Root element #root not found');
|
||||
const root = createRoot(container);
|
||||
root.render(<AutoplayApp />);
|
||||
|
||||
@@ -62,6 +62,8 @@ export function AttachmentLightbox({
|
||||
: null;
|
||||
const isOpen = current !== null;
|
||||
const hasMultiple = items.length > 1;
|
||||
// 1-based position for the "x / N" counter; null when nothing is open.
|
||||
const position = openIndex !== null ? openIndex + 1 : null;
|
||||
|
||||
// Remote items resolve through the signed-URL cache; disabled when not remote.
|
||||
const remoteObjectId =
|
||||
@@ -107,7 +109,7 @@ export function AttachmentLightbox({
|
||||
if (wasLast) {
|
||||
onOpenChange(null);
|
||||
} else if (wasAtEnd) {
|
||||
onOpenChange(openIndex! - 1);
|
||||
onOpenChange(items.length - 2);
|
||||
}
|
||||
// Otherwise openIndex stays — the next item shifts into its place.
|
||||
};
|
||||
@@ -187,7 +189,7 @@ export function AttachmentLightbox({
|
||||
)}
|
||||
{hasMultiple && (
|
||||
<span className="border-l border-white/10 pl-2 text-xs text-white/40">
|
||||
{openIndex! + 1} / {items.length}
|
||||
{position} / {items.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,9 @@ import { useStreamKeyboardNav } from "@/hooks/use-stream-keyboard-nav";
|
||||
*/
|
||||
export default function NetworkRoot() {
|
||||
const { networkId } = useParams();
|
||||
if (!networkId) throw new Error("NetworkRoot requires a :networkId route param");
|
||||
const navigate = useNavigate();
|
||||
const path = particlePath(networkId!, []);
|
||||
const path = particlePath(networkId, []);
|
||||
|
||||
const [composeActive, setComposeActive] = useState(false);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -68,7 +69,7 @@ export default function NetworkRoot() {
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pb-14 py-2">
|
||||
<ParticleListView
|
||||
streams={streams}
|
||||
networkId={networkId!}
|
||||
networkId={networkId}
|
||||
isLoading={isLoading}
|
||||
selectedIndex={selectedIndex}
|
||||
canLoadMore={canLoadMore}
|
||||
@@ -76,10 +77,10 @@ export default function NetworkRoot() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ComposeOverlay networkId={networkId!} onActiveChange={setComposeActive} />
|
||||
<ComposeOverlay networkId={networkId} onActiveChange={setComposeActive} />
|
||||
{!composeActive && (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-16 z-20 flex justify-center px-3">
|
||||
<ComposeQuotaIndicator networkId={networkId!} />
|
||||
<ComposeQuotaIndicator networkId={networkId} />
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 flex justify-center p-3">
|
||||
|
||||
@@ -179,14 +179,15 @@ function Section({ children }: { children: React.ReactNode }) {
|
||||
export default function NetworkSettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { networkId } = useParams<{ networkId: string }>();
|
||||
if (!networkId) throw new Error("NetworkSettingsPage requires a :networkId route param");
|
||||
const [searchParams] = useSearchParams();
|
||||
const { data: networks } = useNetworks();
|
||||
const network = networks?.find((n) => n.id === networkId);
|
||||
const { data: invitations, error: invitationsError } = useNetworkInvitations(networkId!);
|
||||
const { data: invitations, error: invitationsError } = useNetworkInvitations(networkId);
|
||||
const currentUser = useAuthStore((s) => s.user);
|
||||
const isAdmin = currentUser?.id === network?.admin_human.id;
|
||||
const [memberToRemove, setMemberToRemove] = useState<Human | null>(null);
|
||||
const removeMember = useRemoveMember(networkId!);
|
||||
const removeMember = useRemoveMember(networkId);
|
||||
|
||||
const billingRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -279,7 +280,7 @@ export default function NetworkSettingsPage() {
|
||||
}
|
||||
/>
|
||||
<Separator />
|
||||
<InviteForm networkId={networkId!} />
|
||||
<InviteForm networkId={networkId} />
|
||||
{invitationsError && (
|
||||
<>
|
||||
<Separator />
|
||||
@@ -288,7 +289,7 @@ export default function NetworkSettingsPage() {
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{pendingCount > 0 && (
|
||||
{invitations && invitations.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="px-4 pb-1 pt-3">
|
||||
@@ -296,13 +297,13 @@ export default function NetworkSettingsPage() {
|
||||
Pending
|
||||
</Muted>
|
||||
</div>
|
||||
{invitations!.map((inv, index) => (
|
||||
{invitations.map((inv, index) => (
|
||||
<div key={inv.email}>
|
||||
<PendingInvitationRow
|
||||
email={inv.email}
|
||||
networkId={networkId!}
|
||||
networkId={networkId}
|
||||
/>
|
||||
{index < invitations!.length - 1 && (
|
||||
{index < invitations.length - 1 && (
|
||||
<Separator className="mx-4" />
|
||||
)}
|
||||
</div>
|
||||
@@ -324,7 +325,7 @@ export default function NetworkSettingsPage() {
|
||||
}
|
||||
/>
|
||||
<Separator />
|
||||
<BillingSection networkId={networkId!} />
|
||||
<BillingSection networkId={networkId} />
|
||||
</Section>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import { FolderView } from "@/features/particles/folder-view";
|
||||
*/
|
||||
export default function ParticleViewResolver() {
|
||||
const { networkId, "*": rest } = useParams();
|
||||
if (!networkId) throw new Error("ParticleViewResolver requires a :networkId route param");
|
||||
const segments = (rest ?? "").split("/").filter(Boolean);
|
||||
const path = particlePath(networkId!, segments); // path of current container particle
|
||||
const path = particlePath(networkId, segments); // path of current container particle
|
||||
|
||||
const { particle, isLoading, error } = useLiveParticle(path);
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ export function ReactionBar({
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
{/* Emoji reaction pills */}
|
||||
{activeEmojis.map((emoji) => {
|
||||
const reactors = reactions![emoji];
|
||||
const reactors = reactions?.[emoji] ?? [];
|
||||
const isMine = reactors.includes(currentHumanId);
|
||||
return (
|
||||
<Tooltip key={emoji}>
|
||||
@@ -88,7 +88,7 @@ export function ReactionBar({
|
||||
|
||||
{/* Text reaction pills */}
|
||||
{activeTextReactions.map((text) => {
|
||||
const reactors = reactions![text];
|
||||
const reactors = reactions?.[text] ?? [];
|
||||
const isMine = reactors.includes(currentHumanId);
|
||||
const firstReactor = resolveHumanDisplay(reactors[0], humans);
|
||||
const reactorList = getReactorList(reactors, humans, currentHumanId);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation, skipToken } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/api/client";
|
||||
import type { BillingCadence } from "@/api/types";
|
||||
|
||||
export function useNetworkBilling(networkId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["network-billing", networkId],
|
||||
queryFn: () => apiClient.getNetworkBilling(networkId!),
|
||||
enabled: !!networkId,
|
||||
queryFn: networkId
|
||||
? () => apiClient.getNetworkBilling(networkId)
|
||||
: skipToken,
|
||||
// Refetch on window focus so the UI catches up after the user returns
|
||||
// from Stripe Checkout (webhook may land a second or two later).
|
||||
// FIX: doesn't work with electron
|
||||
|
||||
@@ -36,11 +36,11 @@ export function useChannel(channelId: string | null): UseChannelResult {
|
||||
};
|
||||
|
||||
const onJoin = (msg: { humanId?: string }) => {
|
||||
if (msg.humanId) {
|
||||
setPresence((prev) =>
|
||||
prev.includes(msg.humanId!) ? prev : [...prev, msg.humanId!],
|
||||
);
|
||||
}
|
||||
const humanId = msg.humanId;
|
||||
if (!humanId) return;
|
||||
setPresence((prev) =>
|
||||
prev.includes(humanId) ? prev : [...prev, humanId],
|
||||
);
|
||||
};
|
||||
|
||||
const onLeave = (msg: { humanId?: string }) => {
|
||||
@@ -50,12 +50,9 @@ export function useChannel(channelId: string | null): UseChannelResult {
|
||||
};
|
||||
|
||||
const onMessage = (msg: { humanId?: string; payload?: unknown }) => {
|
||||
if (msg.humanId) {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ humanId: msg.humanId!, payload: msg.payload },
|
||||
]);
|
||||
}
|
||||
const humanId = msg.humanId;
|
||||
if (!humanId) return;
|
||||
setMessages((prev) => [...prev, { humanId, payload: msg.payload }]);
|
||||
};
|
||||
|
||||
client.on(channelId, "subscribed", onSubscribed);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, skipToken } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/api/client";
|
||||
|
||||
export function useDownloadUrl(objectId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ["download-url", objectId],
|
||||
queryFn: () => apiClient.getParticleDownloadUrl(objectId!),
|
||||
enabled: !!objectId,
|
||||
queryFn: objectId
|
||||
? () => apiClient.getParticleDownloadUrl(objectId)
|
||||
: skipToken,
|
||||
staleTime: 1000 * 60 * 60, // 1 hour — signed URLs valid for 24h
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useQueries, useQuery } from "@tanstack/react-query";
|
||||
import { useQueries, useQuery, skipToken } from "@tanstack/react-query";
|
||||
import { extractUrls, type LinkMetadata } from "@/lib/link-metadata";
|
||||
import { platform } from "@/lib/platform";
|
||||
|
||||
export function useLinkMetadata(url: string | null) {
|
||||
return useQuery<LinkMetadata | null>({
|
||||
queryKey: ["link-metadata", url],
|
||||
queryFn: () => platform.link.fetchMetadata(url!),
|
||||
enabled: !!url,
|
||||
queryFn: url ? () => platform.link.fetchMetadata(url) : skipToken,
|
||||
staleTime: Infinity,
|
||||
gcTime: 30 * 60 * 1000,
|
||||
retry: 1,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient, skipToken } from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
import { apiClient } from "@/api/client";
|
||||
import type { NetworkUsage } from "@/api/types";
|
||||
@@ -9,8 +9,9 @@ export const networkUsageQueryKey = (networkId: string | undefined) =>
|
||||
export function useNetworkUsage(networkId: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: networkUsageQueryKey(networkId),
|
||||
queryFn: () => apiClient.getNetworkUsage(networkId!),
|
||||
enabled: !!networkId,
|
||||
queryFn: networkId
|
||||
? () => apiClient.getNetworkUsage(networkId)
|
||||
: skipToken,
|
||||
// Refetch whenever a consumer mounts (billing settings, compose indicator)
|
||||
// so users land on fresh quota state without listener wiring.
|
||||
refetchOnMount: "always",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, type RefObject } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { MediaParticleHandle } from "@/features/particles/media-particle-view";
|
||||
import { selectIsPaused, usePlaybackPauseStore } from "@/stores/playback-pause-store";
|
||||
import { isTypingTarget } from "@/lib/keyboard";
|
||||
|
||||
const SEEK_DELTA_SEC = 5;
|
||||
|
||||
@@ -181,7 +181,7 @@ export function useStreamPlayback(
|
||||
lastPersistedMarkerRef.current = currentTime;
|
||||
const streamDocPath = toFirestoreDocPath(path);
|
||||
updateStreamPlaybackMarker(streamDocPath, userId, currentTime);
|
||||
}, [currentParticle?.id, state.initialized, userId, path]);
|
||||
}, [currentParticle, streamParticle.playback_markers, state.initialized, userId, path]);
|
||||
|
||||
// --- Navigation callbacks ---
|
||||
const next = useCallback(() => {
|
||||
|
||||
@@ -110,7 +110,7 @@ function HuddleContent() {
|
||||
lastAutoPin.current = null;
|
||||
}
|
||||
}, [
|
||||
screenShareTracks.map((t) => `${t.publication.trackSid}_${t.publication.isSubscribed}`).join(),
|
||||
layoutContext.pin, screenShareTracks
|
||||
]);
|
||||
|
||||
// Sync isSharing state with actual track state
|
||||
@@ -125,6 +125,15 @@ function HuddleContent() {
|
||||
};
|
||||
}, [room]);
|
||||
|
||||
const stopScreenShare = useCallback(async () => {
|
||||
const pub = room.localParticipant.getTrackPublication(Track.Source.ScreenShare);
|
||||
if (pub?.track) {
|
||||
await room.localParticipant.unpublishTrack(pub.track.mediaStreamTrack);
|
||||
pub.track.stop();
|
||||
}
|
||||
setIsSharing(false);
|
||||
}, [room]);
|
||||
|
||||
const handleScreenShare = useCallback(
|
||||
async (sourceId: string) => {
|
||||
setShowPicker(false);
|
||||
@@ -148,18 +157,9 @@ function HuddleContent() {
|
||||
logError(err, { scope: "huddle.screenShare" });
|
||||
}
|
||||
},
|
||||
[room],
|
||||
[room, stopScreenShare],
|
||||
);
|
||||
|
||||
const stopScreenShare = useCallback(async () => {
|
||||
const pub = room.localParticipant.getTrackPublication(Track.Source.ScreenShare);
|
||||
if (pub?.track) {
|
||||
await room.localParticipant.unpublishTrack(pub.track.mediaStreamTrack);
|
||||
pub.track.stop();
|
||||
}
|
||||
setIsSharing(false);
|
||||
}, [room]);
|
||||
|
||||
return (
|
||||
<div className="lk-video-conference">
|
||||
<LayoutContextProvider value={layoutContext} onWidgetChange={setWidgetState}>
|
||||
|
||||
@@ -5,5 +5,7 @@ import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('Root element #root not found');
|
||||
const root = createRoot(container);
|
||||
root.render(<HuddleApp />);
|
||||
|
||||
@@ -301,7 +301,7 @@ export async function updateParticleProperties<T extends ParticleType>(
|
||||
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, any> = {};
|
||||
const updatedProperties: Record<string, any> = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const key in properties) {
|
||||
updatedProperties[`properties.${key}`] = properties[key];
|
||||
}
|
||||
@@ -352,7 +352,7 @@ export async function updateParticleVisibleTo(
|
||||
export async function updateParticle(
|
||||
docPath: string,
|
||||
fieldName: string,
|
||||
value: any,
|
||||
value: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
): Promise<void> {
|
||||
const particleRef = typedDoc(docPath);
|
||||
await updateDoc(particleRef, {
|
||||
|
||||
@@ -15,7 +15,8 @@ export async function createImageThumbnail(
|
||||
const h = Math.round(bitmap.height * scale);
|
||||
|
||||
const canvas = new OffscreenCanvas(w, h);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Failed to acquire 2D canvas context");
|
||||
ctx.drawImage(bitmap, 0, 0, w, h);
|
||||
bitmap.close();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
import { useAutoplayPayloadStore } from "@/stores/autoplay-payload-store";
|
||||
import type { AutoplayPayload } from "@/lib/autoplay-ipc";
|
||||
import { apiClient } from "@/api/client";
|
||||
|
||||
@@ -138,14 +138,17 @@ export class PusherClient {
|
||||
event: ChannelEventType,
|
||||
callback: ChannelEventCallback,
|
||||
): void {
|
||||
if (!this.listeners.has(channelId)) {
|
||||
this.listeners.set(channelId, new Map());
|
||||
let channelListeners = this.listeners.get(channelId);
|
||||
if (!channelListeners) {
|
||||
channelListeners = new Map();
|
||||
this.listeners.set(channelId, channelListeners);
|
||||
}
|
||||
const channelListeners = this.listeners.get(channelId)!;
|
||||
if (!channelListeners.has(event)) {
|
||||
channelListeners.set(event, new Set());
|
||||
let eventListeners = channelListeners.get(event);
|
||||
if (!eventListeners) {
|
||||
eventListeners = new Set();
|
||||
channelListeners.set(event, eventListeners);
|
||||
}
|
||||
channelListeners.get(event)!.add(callback);
|
||||
eventListeners.add(callback);
|
||||
}
|
||||
|
||||
off(
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
@@ -15,27 +15,24 @@ const PusherStateContext = createContext<ConnectionState>("disconnected");
|
||||
|
||||
export function PusherProvider({ children }: { children: ReactNode }) {
|
||||
const token = useSessionStore((s) => s.token);
|
||||
const clientRef = useRef<PusherClient | null>(null);
|
||||
const [connectionState, setConnectionState] =
|
||||
useState<ConnectionState>("disconnected");
|
||||
|
||||
useEffect(() => {
|
||||
const client = useMemo(() => {
|
||||
if (!token) {
|
||||
// Disconnect if token is cleared (logout)
|
||||
if (clientRef.current) {
|
||||
clientRef.current.disconnect();
|
||||
clientRef.current = null;
|
||||
setConnectionState("disconnected");
|
||||
}
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const client = new PusherClient({
|
||||
return new PusherClient({
|
||||
url: appConfig.pusherUrl,
|
||||
getToken: () => useSessionStore.getState().token,
|
||||
});
|
||||
}, [token]);
|
||||
|
||||
clientRef.current = client;
|
||||
useEffect(() => {
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeState = client.onStateChange((state) => {
|
||||
setConnectionState(state);
|
||||
@@ -46,12 +43,12 @@ export function PusherProvider({ children }: { children: ReactNode }) {
|
||||
return () => {
|
||||
unsubscribeState();
|
||||
client.disconnect();
|
||||
clientRef.current = null;
|
||||
setConnectionState("disconnected");
|
||||
};
|
||||
}, [token]);
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<PusherContext.Provider value={clientRef.current}>
|
||||
<PusherContext.Provider value={client}>
|
||||
<PusherStateContext.Provider value={connectionState}>
|
||||
{children}
|
||||
</PusherStateContext.Provider>
|
||||
|
||||
@@ -79,7 +79,7 @@ class SoundEffectsEngine {
|
||||
*/
|
||||
preload(name: string, url: string): Promise<AudioBuffer | null> {
|
||||
if (this.buffers.has(name)) {
|
||||
return Promise.resolve(this.buffers.get(name)!);
|
||||
return Promise.resolve(this.buffers.get(name));
|
||||
}
|
||||
const existing = this.loading.get(name);
|
||||
if (existing) return existing;
|
||||
|
||||
@@ -4,7 +4,6 @@ import started from 'electron-squirrel-startup';
|
||||
import { updateElectronApp, UpdateSourceType } from 'update-electron-app';
|
||||
|
||||
import { appConfig } from './config/env';
|
||||
import { logError } from './lib/errors';
|
||||
import { safeHandle } from './main/ipc-utils';
|
||||
import { initSentryMain } from './main/sentry';
|
||||
|
||||
|
||||
@@ -5,5 +5,7 @@ import './styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('Root element #root not found');
|
||||
const root = createRoot(container);
|
||||
root.render(<App />);
|
||||
|
||||
@@ -5,5 +5,7 @@ import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('Root element #root not found');
|
||||
const root = createRoot(container);
|
||||
root.render(<ScreenRecordControlApp />);
|
||||
|
||||
@@ -5,5 +5,7 @@ import "@/styles/globals.css";
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
const root = createRoot(document.getElementById("root")!);
|
||||
const container = document.getElementById("root");
|
||||
if (!container) throw new Error("Root element #root not found");
|
||||
const root = createRoot(container);
|
||||
root.render(<App />);
|
||||
|
||||
Reference in New Issue
Block a user