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
-5
View File
@@ -1,5 +0,0 @@
export const isMac = window.electronWindow?.platform === "darwin";
// Symbol to show in keyboard hints for the primary modifier
// (Cmd on macOS, Ctrl on Windows/Linux).
export const metaKey = isMac ? "⌘" : "Ctrl";
@@ -0,0 +1,18 @@
import { toast } from "sonner";
import { platform, DESKTOP_DOWNLOAD_URL } from "@/lib/platform";
/**
* Gate desktop-only features (huddle, screen recording). On Electron this
* always returns true. On web it shows a toast linking to the desktop app
* download and returns false — callers should bail.
*/
export function requireDesktop(feature: string): boolean {
if (platform.kind === "electron") return true;
toast.message(`${feature} is only available in the desktop app`, {
action: {
label: "Download",
onClick: () => window.open(DESKTOP_DOWNLOAD_URL, "_blank", "noopener,noreferrer"),
},
});
return false;
}
+57
View File
@@ -0,0 +1,57 @@
import { apiClient } from "@/api/client";
import type { Platform } from "./types";
export const electronPlatform: Platform = {
kind: "electron",
window: {
minimize: () => window.electronWindow.minimize(),
maximize: () => window.electronWindow.maximize(),
fullscreen: () => window.electronWindow.fullscreen(),
close: () => window.electronWindow.close(),
onMaximizeChange: (cb) => window.electronWindow.onMaximizeChange(cb),
get platform() {
const p = window.electronWindow?.platform;
return p === "darwin" || p === "win32" || p === "linux" ? p : "linux";
},
},
huddle: {
isSupported: true,
open: (args) => window.electronWindow.openHuddle(args),
close: () => window.electronWindow.closeHuddle(),
getScreenSources: () => window.electronHuddle.getScreenSources(),
},
screenRecord: {
isSupported: true,
start: () => window.electronScreen.startRecordingWindow(),
stop: () => window.electronScreenRecord.stop(),
cancel: () => window.electronScreen.stopRecordingWindow(),
onStopRequested: (cb) => window.electronScreen.onStopRequested(cb),
getScreenSources: () => window.electronScreen.getScreenSources(),
},
autoplay: {
play: (payload) => window.electronAutoplay.play(payload),
dismiss: () => window.electronAutoplay.dismiss(),
navigate: (d) => window.electronAutoplay.navigate(d),
onPlay: (cb) => window.electronAutoplay.onPlay(cb),
onStop: (cb) => window.electronAutoplay.onStop(cb),
onNavigate: (cb) => window.electronAutoplay.onNavigate(cb),
},
link: {
fetchMetadata: (url) => apiClient.getLinkMetadata(url).catch(() => null),
openExternal: (url) => window.electronLink.openExternal(url),
},
attachment: {
download: (url, filename) => window.electronAttachment.download(url, filename),
},
app: {
setDockBadge: (count) => window.electronApp.setDockBadge(count),
getVersion: () => window.electronApp.getVersion(),
},
};
+8
View File
@@ -0,0 +1,8 @@
import { electronPlatform } from "./electron";
export const platform = electronPlatform;
export type { Platform, ScreenSource } from "./types";
export { DESKTOP_DOWNLOAD_URL } from "./types";
export const isMac = platform.window.platform === "darwin";
export const metaKey = isMac ? "⌘" : "Ctrl";
+8
View File
@@ -0,0 +1,8 @@
import { webPlatform } from "./web";
export const platform = webPlatform;
export type { Platform, ScreenSource } from "./types";
export { DESKTOP_DOWNLOAD_URL } from "./types";
export const isMac = platform.window.platform === "darwin";
export const metaKey = isMac ? "⌘" : "Ctrl";
+63
View File
@@ -0,0 +1,63 @@
import type { AutoplayPayload } from "@/lib/autoplay-ipc";
import type { LinkMetadata } from "@/lib/link-metadata";
export interface ScreenSource {
id: string;
name: string;
thumbnailDataUrl: string;
appIconDataUrl: string | null;
}
export interface Platform {
kind: "electron" | "web";
window: {
minimize: () => void;
maximize: () => void;
fullscreen: () => void;
close: () => void;
onMaximizeChange: (cb: (m: boolean) => void) => () => void;
platform: "darwin" | "win32" | "linux" | "web";
};
huddle: {
isSupported: boolean;
open: (args: { token: string; serverUrl: string }) => void;
close: () => void;
getScreenSources: () => Promise<ScreenSource[]>;
};
screenRecord: {
isSupported: boolean;
start: () => void;
stop: () => void;
cancel: () => void;
onStopRequested: (cb: () => void) => () => void;
getScreenSources: () => Promise<ScreenSource[]>;
};
autoplay: {
play: (payload: AutoplayPayload) => void;
dismiss: () => void;
navigate: (d: { networkId: string; streamId: string }) => void;
onPlay: (cb: (payload: AutoplayPayload) => void) => () => void;
onStop: (cb: () => void) => () => void;
onNavigate: (cb: (d: { networkId: string; streamId: string }) => void) => () => void;
};
link: {
fetchMetadata: (url: string) => Promise<LinkMetadata | null>;
openExternal: (url: string) => Promise<void>;
};
attachment: {
download: (url: string, filename?: string) => void;
};
app: {
setDockBadge: (count: number) => void;
getVersion: () => Promise<string>;
};
}
export const DESKTOP_DOWNLOAD_URL = "https://flowylabs.ai/llink/download";
+107
View File
@@ -0,0 +1,107 @@
import { useAutoplayPayloadStore } from "@/stores/autoplay-payload-store";
import type { AutoplayPayload } from "@/lib/autoplay-ipc";
import { apiClient } from "@/api/client";
import type { Platform } from "./types";
declare const __APP_VERSION__: string;
function detectWebPlatform(): "darwin" | "win32" | "linux" | "web" {
if (typeof navigator === "undefined") return "web";
const ua = navigator.userAgent;
if (/Mac|iPhone|iPad|iPod/i.test(ua)) return "darwin";
if (/Win/i.test(ua)) return "win32";
if (/Linux|X11/i.test(ua)) return "linux";
return "web";
}
function downloadCrossOrigin(url: string, filename?: string) {
const a = document.createElement("a");
a.href = url;
if (filename) a.download = filename;
a.target = "_blank";
a.rel = "noopener noreferrer";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
const baseTitle = typeof document !== "undefined" ? document.title : "llink";
function applyDockBadge(count: number) {
if (typeof document === "undefined") return;
document.title = count > 0 ? `(${count}) ${baseTitle}` : baseTitle;
}
const NOT_SUPPORTED = "Not supported in the web app";
export const webPlatform: Platform = {
kind: "web",
window: {
minimize: () => {},
maximize: () => {},
fullscreen: () => {},
close: () => {},
onMaximizeChange: () => () => {},
platform: detectWebPlatform(),
},
huddle: {
isSupported: false,
open: () => { throw new Error(NOT_SUPPORTED); },
close: () => {},
getScreenSources: async () => { throw new Error(NOT_SUPPORTED); },
},
screenRecord: {
isSupported: false,
start: () => { throw new Error(NOT_SUPPORTED); },
stop: () => {},
cancel: () => {},
onStopRequested: () => () => {},
getScreenSources: async () => { throw new Error(NOT_SUPPORTED); },
},
autoplay: {
play: (payload: AutoplayPayload) => {
useAutoplayPayloadStore.getState().setPayload(payload);
},
dismiss: () => {
useAutoplayPayloadStore.getState().setPayload(null);
},
navigate: (d) => {
useAutoplayPayloadStore.getState().setPayload(null);
useAutoplayPayloadStore.getState().setPendingNav(d);
},
onPlay: (cb) =>
useAutoplayPayloadStore.subscribe((state, prev) => {
if (state.payload && state.payload !== prev.payload) cb(state.payload);
}),
onStop: (cb) =>
useAutoplayPayloadStore.subscribe((state, prev) => {
if (!state.payload && prev.payload) cb();
}),
onNavigate: (cb) =>
useAutoplayPayloadStore.subscribe((state, prev) => {
if (state.pendingNav && state.pendingNav !== prev.pendingNav) {
cb({ networkId: state.pendingNav.networkId, streamId: state.pendingNav.streamId });
}
}),
},
link: {
fetchMetadata: (url) => apiClient.getLinkMetadata(url).catch(() => null),
openExternal: async (url) => {
window.open(url, "_blank", "noopener,noreferrer");
},
},
attachment: {
download: downloadCrossOrigin,
},
app: {
setDockBadge: applyDockBadge,
getVersion: async () => __APP_VERSION__,
},
};
+6
View File
@@ -0,0 +1,6 @@
import type { PropsWithChildren } from "react";
import { HashRouter } from "react-router-dom";
export function RouterShell({ children }: PropsWithChildren) {
return <HashRouter>{children}</HashRouter>;
}
+6
View File
@@ -0,0 +1,6 @@
import type { PropsWithChildren } from "react";
import { BrowserRouter } from "react-router-dom";
export function RouterShell({ children }: PropsWithChildren) {
return <BrowserRouter>{children}</BrowserRouter>;
}
+25
View File
@@ -0,0 +1,25 @@
import * as Sentry from "@sentry/react";
import { appConfig, appEnv } from "@/config/env";
import { installErrorSinks } from "@/lib/errors";
export function initSentryRenderer(): void {
if (!appConfig.sentryDsn) return;
Sentry.init({
dsn: appConfig.sentryDsn,
environment: appEnv,
tracesSampleRate: 0,
});
installErrorSinks({
capture: (err, context) =>
Sentry.captureException(err, { extra: context }),
breadcrumb: (err, context) =>
Sentry.addBreadcrumb({
category: "error",
level: "error",
message: err instanceof Error ? err.message : String(err),
data: context,
}),
});
}