feat: ship a web app (#218)

* feat(orion): add endpoint for link metadata

* refactor: cleanup comments

* wip: plumbing for building a web app

* setup deployment materials for web app

* fix(web): favicon
This commit was merged in pull request #218.
This commit is contained in:
Arjun Patel
2026-05-26 15:37:35 -07:00
committed by GitHub
parent 173c63508a
commit 64f02d1c3b
51 changed files with 1260 additions and 238 deletions
@@ -0,0 +1,90 @@
import { useRef } from "react";
import { X } from "lucide-react";
import type { AutoplayPayload } from "@/lib/autoplay-ipc";
interface AutoplayCardContentProps {
payload: AutoplayPayload;
onDismiss: () => void;
onNavigate: () => void;
onEnded: () => void;
}
export function AutoplayCardContent({
payload,
onDismiss,
onNavigate,
onEnded,
}: AutoplayCardContentProps) {
const mediaRef = useRef<HTMLVideoElement | HTMLAudioElement | null>(null);
const isVideo = payload.mimeType.startsWith("video/");
const handleClick = () => {
mediaRef.current?.pause();
onNavigate();
};
const handleClose = (e: React.MouseEvent) => {
e.stopPropagation();
mediaRef.current?.pause();
onDismiss();
};
return (
<div
className="relative h-full w-full cursor-pointer overflow-hidden bg-black"
onClick={handleClick}
>
{isVideo ? (
<>
<video
ref={mediaRef as React.Ref<HTMLVideoElement>}
key={payload.particleId}
src={payload.downloadUrl}
autoPlay
playsInline
onEnded={onEnded}
className="block h-full w-full object-cover"
/>
<button
className="absolute top-1 right-1 z-10 rounded-full bg-black/50 p-0.5 text-white hover:bg-black/70"
onClick={handleClose}
>
<X className="size-3.5" />
</button>
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 py-2">
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-white/20 text-[10px] font-medium text-white">
{payload.senderInitials}
</div>
<p className="truncate text-xs text-white/80">{payload.senderName}</p>
</div>
</>
) : (
<>
<button
className="absolute top-1 right-1 z-10 rounded-full bg-black/50 p-0.5 text-white hover:bg-black/70"
onClick={handleClose}
>
<X className="size-3.5" />
</button>
<div className="flex h-full w-full items-center gap-2 bg-card px-3 py-3">
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
{payload.senderInitials}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-card-foreground">{payload.senderName}</p>
<p className="text-xs text-muted-foreground">Playing audio...</p>
</div>
</div>
<audio
ref={mediaRef as React.Ref<HTMLAudioElement>}
key={payload.particleId}
src={payload.downloadUrl}
autoPlay
onEnded={onEnded}
/>
</>
)}
</div>
);
}
@@ -0,0 +1,42 @@
import { useCallback } from "react";
import { platform } from "@/lib/platform";
import { useAutoplayPayloadStore } from "@/stores/autoplay-payload-store";
import { AutoplayCardContent } from "@/components/autoplay-card-content";
/**
* Bottom-right floating autoplay card used on the web client. The desktop
* client uses a separate BrowserWindow (`src/autoplay_window/`), so this
* component renders nothing on Electron.
*/
export function InAppAutoplayCard() {
const payload = useAutoplayPayloadStore((s) => s.payload);
const setPayload = useAutoplayPayloadStore((s) => s.setPayload);
const setPendingNav = useAutoplayPayloadStore((s) => s.setPendingNav);
const handleDismiss = useCallback(() => setPayload(null), [setPayload]);
const handleNavigate = useCallback(() => {
if (!payload) return;
setPayload(null);
setPendingNav({ networkId: payload.networkId, streamId: payload.streamId });
}, [payload, setPayload, setPendingNav]);
if (platform.kind !== "web") return null;
if (!payload) return null;
const isVideo = payload.mimeType.startsWith("video/");
return (
<div
className="fixed bottom-4 right-4 z-50 w-80 overflow-hidden rounded-lg shadow-lg"
style={{ height: isVideo ? 180 : 64 }}
>
<AutoplayCardContent
payload={payload}
onDismiss={handleDismiss}
onNavigate={handleNavigate}
onEnded={handleDismiss}
/>
</div>
);
}
@@ -3,6 +3,7 @@ import type { LinkMetadata } from "@/lib/link-metadata";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { platform } from "@/lib/platform";
interface LinkPreviewCardProps {
metadata: LinkMetadata;
@@ -12,7 +13,7 @@ interface LinkPreviewCardProps {
export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
const handleOpen = (e: React.MouseEvent) => {
e.stopPropagation();
window.electronLink.openExternal(metadata.url);
platform.link.openExternal(metadata.url);
};
const handleCopy = (e: React.MouseEvent) => {
@@ -3,20 +3,23 @@ import { Copy, Minus, Square, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { platform } from "@/lib/platform";
export function WindowControls() {
const [isMaximized, setIsMaximized] = useState(false);
useEffect(() => {
return window.electronWindow.onMaximizeChange(setIsMaximized);
return platform.window.onMaximizeChange(setIsMaximized);
}, []);
if (platform.kind !== "electron") return null;
const minimize = (
<Button
key="minimize"
variant="ghost"
size="icon-sm"
onClick={() => window.electronWindow.minimize()}
onClick={() => platform.window.minimize()}
aria-label="Minimize"
className="dark:hover:bg-white/10 rounded text-white/50"
>
@@ -29,7 +32,7 @@ export function WindowControls() {
key="maximize"
variant="ghost"
size="icon-sm"
onClick={() => window.electronWindow.maximize()}
onClick={() => platform.window.maximize()}
aria-label={isMaximized ? "Restore" : "Maximize"}
className="dark:hover:bg-white/10 rounded text-white/50"
>
@@ -42,7 +45,7 @@ export function WindowControls() {
key="close"
variant="ghost"
size="icon-sm"
onClick={() => window.electronWindow.close()}
onClick={() => platform.window.close()}
aria-label="Close"
className="hover:text-destructive dark:hover:bg-white/10 rounded text-white/50"
>