wip: plumbing for building a web app

This commit is contained in:
Arjun Patel
2026-05-26 14:37:28 -07:00
parent 1e992abf41
commit 578ebdbd1e
39 changed files with 677 additions and 236 deletions
@@ -12,6 +12,7 @@ import {
import { useDownloadUrl } from "@/hooks/use-download-url";
import { Button } from "@/components/ui/button";
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
import { platform } from "@/lib/platform";
export interface AttachmentItem {
id: string;
@@ -95,7 +96,7 @@ export function AttachmentLightbox({
const handleDownload = () => {
if (!current || !url || current.source.kind !== "remote") return;
window.electronAttachment.download(url, current.filename);
platform.attachment.download(url, current.filename);
};
const handleRemove = () => {
+3 -2
View File
@@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
import { H3, Muted } from "@/components/ui/typography";
import { PRIVACY_URL, TERMS_URL } from "@/lib/constants";
import { useAuthStore } from "@/stores/auth-store";
import { platform } from "@/lib/platform";
interface EmailStepProps {
onCodeSent: (email: string) => void;
@@ -62,7 +63,7 @@ export function EmailStep({ onCodeSent }: EmailStepProps) {
By continuing, you agree to our{" "}
<button
type="button"
onClick={() => window.electronLink.openExternal(TERMS_URL)}
onClick={() => platform.link.openExternal(TERMS_URL)}
className="underline underline-offset-2 hover:text-foreground"
>
Terms of Service
@@ -70,7 +71,7 @@ export function EmailStep({ onCodeSent }: EmailStepProps) {
and{" "}
<button
type="button"
onClick={() => window.electronLink.openExternal(PRIVACY_URL)}
onClick={() => platform.link.openExternal(PRIVACY_URL)}
className="underline underline-offset-2 hover:text-foreground"
>
Privacy Policy
@@ -9,6 +9,7 @@ import {
getAttachmentHandler,
type AttachmentItem,
} from "@/features/attachments/attachment-lightbox";
import { platform } from "@/lib/platform";
export interface PendingAttachment {
id: string;
@@ -120,7 +121,7 @@ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
return (
<button
type="button"
onClick={() => window.electronLink.openExternal(metadata.url)}
onClick={() => platform.link.openExternal(metadata.url)}
className="flex h-16 w-28 shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-lg bg-white/10 px-2 py-1.5 text-left transition-colors hover:bg-white/15"
>
<div className="flex items-center gap-1 text-[10px] text-white/40">
@@ -23,6 +23,8 @@ import { MAX_ATTACHMENT_SIZE_BYTES, MAX_ATTACHMENTS } from "@/lib/constants";
import type { PendingAttachment } from "@/features/compose/attachment-strip";
import { useSuspendPlayback } from "@/hooks/use-suspend-playback";
import { useComposeIntentStore } from "@/stores/compose-intent-store";
import { platform } from "@/lib/platform";
import { requireDesktop } from "@/lib/platform/desktop-only";
export type ComposeStep = "idle" | "picking" | "recording" | "reviewing" | "typing" | "configuring" | "submitting";
@@ -512,6 +514,7 @@ export function ComposeOverlay({
} else if (e.key === "s" || e.key === "S") {
e.preventDefault();
if (!guardIdle()) break;
if (!requireDesktop("Screen recording")) break;
setRecordingSource("screen");
setStepSync("picking");
} else if (e.key === "t" || e.key === "T") {
@@ -594,7 +597,7 @@ export function ComposeOverlay({
<ScreenSourcePicker
title="Record your screen"
confirmLabel="Record"
getSources={window.electronScreen.getScreenSources}
getSources={platform.screenRecord.getScreenSources}
onSelect={handleScreenSourceSelected}
onCancel={cancel}
/>
@@ -1,4 +1,6 @@
import { useCallback, useEffect, useRef } from "react";
import { platform } from "@/lib/platform";
import { requireDesktop } from "@/lib/platform/desktop-only";
const VIDEO_PREFERRED_MIME = "video/webm;codecs=vp9,opus";
const VIDEO_FALLBACK_MIME = "video/webm";
@@ -75,6 +77,7 @@ export function useScreenRecorder({
const startRecording = useCallback(
async (sourceId: string) => {
if (!requireDesktop("Screen recording")) return;
try {
// 1. Screen video
const screenStream = await navigator.mediaDevices.getUserMedia({
@@ -113,7 +116,7 @@ export function useScreenRecorder({
const durationMs = Date.now() - startTimeRef.current;
const blob = new Blob(chunksRef.current, { type: mime });
stopAllTracks();
window.electronScreen.stopRecordingWindow();
platform.screenRecord.cancel();
if (blob.size > 0) {
onFinishRef.current(blob, durationMs, mime);
@@ -123,17 +126,17 @@ export function useScreenRecorder({
recorder.start(1000);
// 4. Show floating control window
window.electronScreen.startRecordingWindow();
platform.screenRecord.start();
// 5. Listen for stop from floating window
cleanupIpcRef.current = window.electronScreen.onStopRequested(() => {
cleanupIpcRef.current = platform.screenRecord.onStopRequested(() => {
if (recorderRef.current?.state === "recording") {
recorderRef.current.stop();
}
});
} catch (err) {
stopAllTracks();
window.electronScreen.stopRecordingWindow();
platform.screenRecord.cancel();
onErrorRef.current(
err instanceof Error ? err.message : "Failed to start screen recording",
);
@@ -157,14 +160,14 @@ export function useScreenRecorder({
}
}
stopAllTracks();
window.electronScreen.stopRecordingWindow();
platform.screenRecord.cancel();
}, [stopAllTracks]);
// Cleanup on unmount
useEffect(() => {
return () => {
stopAllTracks();
window.electronScreen.stopRecordingWindow();
platform.screenRecord.cancel();
};
}, [stopAllTracks]);
+3 -2
View File
@@ -18,6 +18,7 @@ import {
import { useNetworkUsage } from "@/hooks/use-network-usage";
import { useIsNetworkAdmin } from "@/hooks/use-networks";
import type { BillingCadence, BillingStatus } from "@/api/types";
import { platform } from "@/lib/platform";
function formatCents(cents: number): string {
if (cents % 100 === 0) return `$${cents / 100}`;
@@ -171,7 +172,7 @@ function FreeBilling({
const handleUpgrade = () => {
createCheckout.mutate(cadence, {
onSuccess: ({ url }) => window.electronLink.openExternal(url),
onSuccess: ({ url }) => platform.link.openExternal(url),
});
};
@@ -228,7 +229,7 @@ function ProBilling({
const handleManage = () => {
createPortal.mutate(undefined, {
onSuccess: ({ url }) => window.electronLink.openExternal(url),
onSuccess: ({ url }) => platform.link.openExternal(url),
});
};
@@ -10,6 +10,7 @@ import {
getAttachmentHandler,
type AttachmentItem,
} from "@/features/attachments/attachment-lightbox";
import { platform } from "@/lib/platform";
type FileParticle = Extract<Particle, { type: "file" }>;
@@ -46,7 +47,7 @@ function openParticle(
if (getAttachmentHandler(particle.properties.mime_type) === "lightbox") {
onPreview(index);
} else if (url) {
window.electronLink.openExternal(url);
platform.link.openExternal(url);
}
}
@@ -65,7 +66,7 @@ function ImageAttachment({
const handleDownload = (e: React.MouseEvent) => {
e.stopPropagation();
window.electronAttachment.download(url, particle.properties.filename);
platform.attachment.download(url, particle.properties.filename);
};
return (
@@ -113,7 +114,7 @@ function FileAttachment({
const handleDownload = (e: React.MouseEvent) => {
e.stopPropagation();
if (!url) return;
window.electronAttachment.download(url, particle.properties.filename);
platform.attachment.download(url, particle.properties.filename);
};
return (
@@ -25,6 +25,8 @@ import { WindowControls } from "@/components/window-controls";
import { RelativeTimestamp } from "@/components/relative-timestamp";
import { useStreamPresence } from "@/features/particles/stream-presence-context";
import { resolveHumanDisplay } from "@/lib/humans";
import { platform } from "@/lib/platform";
import { requireDesktop } from "@/lib/platform/desktop-only";
function getParticleDisplayName(particle: Particle): string {
switch (particle.type) {
@@ -71,8 +73,9 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
const hasActiveHuddle = huddleParticipants.length > 0;
const handleJoinHuddle = () => {
if (!requireDesktop("Huddle")) return;
apiClient.getLivekitToken(networkId, streamParticle.id).then(({ token, server_url }) => {
window.electronWindow.openHuddle({ token, serverUrl: server_url });
platform.huddle.open({ token, serverUrl: server_url });
});
};
@@ -30,7 +30,8 @@ import { usePlaybackPauseStore, selectIsPaused } from "@/stores/playback-pause-s
import { usePlaybackKeys } from "@/hooks/use-playback-keys";
import { useStreamNavigationKeys } from "@/hooks/use-stream-navigation-keys";
import { useStreamActionKeys } from "@/hooks/use-stream-action-keys";
import { c } from "vite/dist/node/types.d-aGj9QkWt";
import { platform } from "@/lib/platform";
import { requireDesktop } from "@/lib/platform/desktop-only";
function getReactions(particle: Particle): Record<string, string[]> | undefined {
if (isParticleDeleted(particle)) return undefined;
@@ -150,7 +151,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
const navigate = useNavigate();
useMount(() => {
window.electronAutoplay.dismiss();
platform.autoplay.dismiss();
});
const {
@@ -220,8 +221,9 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
});
const handleOpenHuddle = useCallback(() => {
if (!requireDesktop("Huddle")) return;
apiClient.getLivekitToken(networkId, streamParticle.id).then(({ token, server_url }) => {
window.electronWindow.openHuddle({ token, serverUrl: server_url });
platform.huddle.open({ token, serverUrl: server_url });
});
navigate(`/${networkId}`);
}, [networkId, streamParticle.id, navigate]);
+4 -3
View File
@@ -16,6 +16,7 @@ import { logError, toUserMessage } from "@/lib/errors";
import { toast } from "sonner";
import { PRIVACY_URL, SUPPORT_EMAIL, TERMS_URL } from "@/lib/constants";
import { ArrowLeft } from "lucide-react";
import { platform } from "@/lib/platform";
interface SettingsRowProps {
icon: React.ReactNode;
@@ -79,7 +80,7 @@ export default function SettingsPage() {
const [version, setVersion] = useState<string>();
useEffect(() => {
window.electronApp.getVersion().then(setVersion);
platform.app.getVersion().then(setVersion);
}, []);
const handleToggleEmailNotifications = async (checked: boolean) => {
@@ -190,12 +191,12 @@ export default function SettingsPage() {
<SettingsRow
icon={<Shield className="size-4" />}
label="Privacy Policy"
onClick={() => window.electronLink.openExternal(PRIVACY_URL)}
onClick={() => platform.link.openExternal(PRIVACY_URL)}
/>
<SettingsRow
icon={<FileText className="size-4" />}
label="Terms of Service"
onClick={() => window.electronLink.openExternal(TERMS_URL)}
onClick={() => platform.link.openExternal(TERMS_URL)}
/>
</SettingsGroup>