@@ -169,7 +171,7 @@ export function TextEditor({
onKeyDown={handleKeyDown}
placeholder="Type a message..."
className={cn(
- "w-full resize-none border-none bg-transparent p-8 text-center text-white placeholder-white/40 outline-none",
+ 'w-full resize-none border-none bg-transparent p-8 text-center text-white placeholder-white/40 outline-none',
style.size,
style.weight,
)}
@@ -184,8 +186,8 @@ export function TextEditor({
return (
{
- if (recorderRef.current?.state === "recording") {
+ if (recorderRef.current?.state === 'recording') {
recorderRef.current.stop();
}
}, []);
@@ -158,7 +158,7 @@ export function useRecorder({
if (recorderRef.current) {
recorderRef.current.ondataavailable = null;
recorderRef.current.onstop = null;
- if (recorderRef.current.state === "recording") {
+ if (recorderRef.current.state === 'recording') {
recorderRef.current.stop();
}
}
diff --git a/js/desktop/src/features/compose/use-screen-recorder.ts b/js/desktop/src/features/compose/use-screen-recorder.ts
index a512c01..d933fdb 100644
--- a/js/desktop/src/features/compose/use-screen-recorder.ts
+++ b/js/desktop/src/features/compose/use-screen-recorder.ts
@@ -1,9 +1,9 @@
-import { useCallback, useEffect, useRef } from "react";
-import { platform } from "@/lib/platform";
-import { requireDesktop } from "@/lib/platform/desktop-only";
+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";
+const VIDEO_PREFERRED_MIME = 'video/webm;codecs=vp9,opus';
+const VIDEO_FALLBACK_MIME = 'video/webm';
function getScreenMime(): string {
return MediaRecorder.isTypeSupported(VIDEO_PREFERRED_MIME)
@@ -33,7 +33,7 @@ async function getMicStream(
if (
micDeviceId &&
err instanceof Error &&
- (err.name === "OverconstrainedError" || err.name === "NotFoundError")
+ (err.name === 'OverconstrainedError' || err.name === 'NotFoundError')
) {
return navigator.mediaDevices.getUserMedia({ audio: true });
}
@@ -77,14 +77,14 @@ export function useScreenRecorder({
const startRecording = useCallback(
async (sourceId: string) => {
- if (!requireDesktop("Screen recording")) return;
+ if (!requireDesktop('Screen recording')) return;
try {
// 1. Screen video
const screenStream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
- chromeMediaSource: "desktop",
+ chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
},
} as MediaTrackConstraints,
@@ -130,7 +130,7 @@ export function useScreenRecorder({
// 5. Listen for stop from floating window
cleanupIpcRef.current = platform.screenRecord.onStopRequested(() => {
- if (recorderRef.current?.state === "recording") {
+ if (recorderRef.current?.state === 'recording') {
recorderRef.current.stop();
}
});
@@ -138,7 +138,9 @@ export function useScreenRecorder({
stopAllTracks();
platform.screenRecord.cancel();
onErrorRef.current(
- err instanceof Error ? err.message : "Failed to start screen recording",
+ err instanceof Error
+ ? err.message
+ : 'Failed to start screen recording',
);
}
},
@@ -146,7 +148,7 @@ export function useScreenRecorder({
);
const stopRecording = useCallback(() => {
- if (recorderRef.current?.state === "recording") {
+ if (recorderRef.current?.state === 'recording') {
recorderRef.current.stop();
}
}, []);
@@ -155,7 +157,7 @@ export function useScreenRecorder({
if (recorderRef.current) {
recorderRef.current.ondataavailable = null;
recorderRef.current.onstop = null;
- if (recorderRef.current.state === "recording") {
+ if (recorderRef.current.state === 'recording') {
recorderRef.current.stop();
}
}
diff --git a/js/desktop/src/features/layout.tsx b/js/desktop/src/features/layout.tsx
index b5ae8da..6523587 100644
--- a/js/desktop/src/features/layout.tsx
+++ b/js/desktop/src/features/layout.tsx
@@ -1,9 +1,9 @@
-import { WindowControls } from "@/components/window-controls";
-import { Button } from "@/components/ui/button";
-import { useNavigate, useParams } from "react-router-dom";
-import { Home, Settings, Users, Volume2, VolumeOff } from "lucide-react";
-import { Switch } from "@/components/ui/switch";
-import { useAutoplayStore } from "@/stores/autoplay-store";
+import { WindowControls } from '@/components/window-controls';
+import { Button } from '@/components/ui/button';
+import { useNavigate, useParams } from 'react-router-dom';
+import { Home, Settings, Users, Volume2, VolumeOff } from 'lucide-react';
+import { Switch } from '@/components/ui/switch';
+import { useAutoplayStore } from '@/stores/autoplay-store';
import {
Breadcrumb,
BreadcrumbItem,
@@ -11,31 +11,31 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb";
-import { Avatar, AvatarFallback } from "@/components/ui/avatar";
-import { useNetworks } from "@/hooks/use-networks";
-import { particlePath } from "@/lib/particle-path";
-import { useParticle } from "@/hooks/use-particle";
-import type { Particle } from "@/api/types";
-import { PropsWithChildren, useCallback } from "react";
-import { useDockBadge } from "@/hooks/use-dock-badge";
-import { toast } from "sonner";
-import { RouteErrorBoundary } from "@/components/app-error-boundary";
+} from '@/components/ui/breadcrumb';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+import { useNetworks } from '@/hooks/use-networks';
+import { particlePath } from '@/lib/particle-path';
+import { useParticle } from '@/hooks/use-particle';
+import type { Particle } from '@/api/types';
+import { PropsWithChildren, useCallback } from 'react';
+import { useDockBadge } from '@/hooks/use-dock-badge';
+import { toast } from 'sonner';
+import { RouteErrorBoundary } from '@/components/app-error-boundary';
function getParticleDisplayName(particle: Particle): string {
switch (particle.type) {
- case "stream":
- case "folder":
+ case 'stream':
+ case 'folder':
return particle.properties.name;
- case "quest":
+ case 'quest':
return particle.properties.title;
- case "paper":
+ case 'paper':
return particle.properties.title;
- case "file":
+ case 'file':
return particle.properties.filename;
- case "text":
+ case 'text':
return particle.properties.content.slice(0, 30);
- case "media":
+ case 'media':
return particle.type;
}
}
@@ -65,19 +65,23 @@ function AutoplayToggle() {
const handleToggle = useCallback(() => {
toggleMuted();
if (muted) {
- toast.success("Auto-play enabled");
+ toast.success('Auto-play enabled');
} else {
- toast.info("Auto-play disabled");
+ toast.info('Auto-play disabled');
}
}, [toggleMuted, muted]);
return (
- {muted ? : }
+ {muted ? (
+
+ ) : (
+
+ )}
);
@@ -85,10 +89,13 @@ function AutoplayToggle() {
function TopBar() {
const navigate = useNavigate();
- const { networkId, "*": rest } = useParams();
- const segments = [networkId, ...rest?.split("/") ?? []].filter(Boolean);
+ const { networkId, '*': rest } = useParams();
+ const segments = [networkId, ...(rest?.split('/') ?? [])].filter(Boolean);
- const path = rest && networkId ? particlePath(networkId, rest.split("/").filter(Boolean)) : undefined;
+ const path =
+ rest && networkId
+ ? particlePath(networkId, rest.split('/').filter(Boolean))
+ : undefined;
const { data: particle } = useParticle(path);
@@ -106,7 +113,7 @@ function TopBar() {
) : (
navigate("/")}
+ onClick={() => navigate('/')}
>
@@ -137,7 +144,9 @@ function TopBar() {
- {getParticleDisplayName(particle)}
+
+ {getParticleDisplayName(particle)}
+
)}
@@ -163,7 +172,7 @@ function TopBar() {
variant="ghost"
size="sm"
className="no-drag text-muted-foreground"
- onClick={() => navigate("/settings")}
+ onClick={() => navigate('/settings')}
>
diff --git a/js/desktop/src/features/network-billing.tsx b/js/desktop/src/features/network-billing.tsx
index 7180712..e3c1608 100644
--- a/js/desktop/src/features/network-billing.tsx
+++ b/js/desktop/src/features/network-billing.tsx
@@ -1,24 +1,24 @@
-import { useState } from "react";
-import { ExternalLink } from "lucide-react";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
-import { Separator } from "@/components/ui/separator";
-import { Muted } from "@/components/ui/typography";
-import { CopyableEmail } from "@/components/copyable-email";
-import { cn } from "@/lib/utils";
-import { toUserMessage } from "@/lib/errors";
-import { SUPPORT_EMAIL } from "@/lib/constants";
+import { useState } from 'react';
+import { ExternalLink } from 'lucide-react';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Label } from '@/components/ui/label';
+import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
+import { Separator } from '@/components/ui/separator';
+import { Muted } from '@/components/ui/typography';
+import { CopyableEmail } from '@/components/copyable-email';
+import { cn } from '@/lib/utils';
+import { toUserMessage } from '@/lib/errors';
+import { SUPPORT_EMAIL } from '@/lib/constants';
import {
useCreateCheckoutSession,
useCreatePortalSession,
useNetworkBilling,
-} from "@/hooks/use-billing";
-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";
+} from '@/hooks/use-billing';
+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}`;
@@ -27,17 +27,17 @@ function formatCents(cents: number): string {
function formatDate(date: Date): string {
return date.toLocaleDateString(undefined, {
- month: "long",
- day: "numeric",
- year: "numeric",
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
});
}
-function PlanStatusBadge({ status }: { status: BillingStatus["plan_status"] }) {
- if (status === "past_due")
+function PlanStatusBadge({ status }: { status: BillingStatus['plan_status'] }) {
+ if (status === 'past_due')
return
Past due ;
- if (status === "canceled") return
Canceled ;
- if (status === "trialing") return
Trialing ;
+ if (status === 'canceled') return
Canceled ;
+ if (status === 'trialing') return
Trialing ;
return null;
}
@@ -76,8 +76,8 @@ function CadenceOption({
@@ -98,15 +98,15 @@ function CadenceOption({
function formatResetLocal(resetAt: Date): string {
const time = resetAt.toLocaleTimeString(undefined, {
- hour: "numeric",
- minute: "2-digit",
+ hour: 'numeric',
+ minute: '2-digit',
});
const now = new Date();
const isSameDay =
resetAt.getFullYear() === now.getFullYear() &&
resetAt.getMonth() === now.getMonth() &&
resetAt.getDate() === now.getDate();
- return `${isSameDay ? "today" : "tomorrow"} at ${time}`;
+ return `${isSameDay ? 'today' : 'tomorrow'} at ${time}`;
}
/**
@@ -119,7 +119,7 @@ function PlanSummary({ networkId }: { networkId: string }) {
if (!usage) return null;
- const isPro = usage.plan === "pro";
+ const isPro = usage.plan === 'pro';
return (
<>
@@ -127,9 +127,9 @@ function PlanSummary({ networkId }: { networkId: string }) {
label="Plan"
value={
- {isPro ? "Llink Pro" : "Llink Free"}
-
- {isPro ? "Pro" : "Free"}
+ {isPro ? 'Llink Pro' : 'Llink Free'}
+
+ {isPro ? 'Pro' : 'Free'}
}
@@ -168,7 +168,7 @@ function FreeBilling({
billing: BillingStatus;
}) {
const createCheckout = useCreateCheckoutSession(networkId);
- const [cadence, setCadence] = useState("annual");
+ const [cadence, setCadence] = useState('annual');
const handleUpgrade = () => {
createCheckout.mutate(cadence, {
@@ -194,7 +194,7 @@ function FreeBilling({
perSeatCents={annualPerSeatMonthlyCents}
billedNote="Billed annually"
saveBadge={savingsPct > 0 ? `Save ${savingsPct}%` : undefined}
- selected={cadence === "annual"}
+ selected={cadence === 'annual'}
/>
@@ -211,7 +211,7 @@ function FreeBilling({
onClick={handleUpgrade}
disabled={createCheckout.isPending}
>
- {createCheckout.isPending ? "Opening Stripe..." : "Upgrade to Pro"}
+ {createCheckout.isPending ? 'Opening Stripe...' : 'Upgrade to Pro'}
>
@@ -233,9 +233,9 @@ function ProBilling({
});
};
- const cadenceLabel = billing.cadence === "annual" ? "Annual" : "Monthly";
+ const cadenceLabel = billing.cadence === 'annual' ? 'Annual' : 'Monthly';
const perSeatCents =
- billing.cadence === "annual"
+ billing.cadence === 'annual'
? Math.round(billing.price_annual_cents / 12)
: billing.price_monthly_cents;
const renewal = billing.current_period_end
@@ -249,7 +249,7 @@ function ProBilling({
Your subscription is set to downgrade to Free on {renewal}.
)}
- {billing.plan_status === "past_due" && (
+ {billing.plan_status === 'past_due' && (
Your last payment failed. Update your payment method to keep Pro
active.
@@ -271,7 +271,7 @@ function ProBilling({
<>
>
@@ -284,9 +284,7 @@ function ProBilling({
disabled={createPortal.isPending}
>
- {createPortal.isPending
- ? "Opening Stripe..."
- : "Manage subscription"}
+ {createPortal.isPending ? 'Opening Stripe...' : 'Manage subscription'}
>
@@ -340,11 +338,13 @@ function AdminBillingControls({ networkId }: { networkId: string }) {
if (error) {
return (
- Couldn't load billing: {toUserMessage(error)}
+
+ Couldn't load billing: {toUserMessage(error)}
+
);
}
- if (billing.plan === "pro") {
+ if (billing.plan === 'pro') {
return
;
}
return
;
diff --git a/js/desktop/src/features/network-root.tsx b/js/desktop/src/features/network-root.tsx
index b00e0da..17d0f00 100644
--- a/js/desktop/src/features/network-root.tsx
+++ b/js/desktop/src/features/network-root.tsx
@@ -1,15 +1,15 @@
-import { useCallback, useState } from "react";
-import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import { CircleDot, CircleCheckBig } from "lucide-react";
-import { particlePath } from "@/lib/particle-path";
-import { ParticleListView } from "@/features/particles/particle-list-view";
-import { VideoAudioToggle } from "@/components/video-audio-toggle";
-import { ComposeOverlay } from "./compose/compose-overlay";
-import { useComposeIntentStore } from "@/stores/compose-intent-store";
-import { ComposeQuotaIndicator } from "./compose/compose-quota-indicator";
-import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { useStreamParticles } from "@/hooks/use-stream-particles";
-import { useStreamKeyboardNav } from "@/hooks/use-stream-keyboard-nav";
+import { useCallback, useState } from 'react';
+import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
+import { CircleDot, CircleCheckBig } from 'lucide-react';
+import { particlePath } from '@/lib/particle-path';
+import { ParticleListView } from '@/features/particles/particle-list-view';
+import { VideoAudioToggle } from '@/components/video-audio-toggle';
+import { ComposeOverlay } from './compose/compose-overlay';
+import { useComposeIntentStore } from '@/stores/compose-intent-store';
+import { ComposeQuotaIndicator } from './compose/compose-quota-indicator';
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useStreamParticles } from '@/hooks/use-stream-particles';
+import { useStreamKeyboardNav } from '@/hooks/use-stream-keyboard-nav';
/**
* Route-level component for /:networkId (index).
@@ -17,29 +17,33 @@ 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");
+ if (!networkId)
+ throw new Error('NetworkRoot requires a :networkId route param');
const navigate = useNavigate();
const path = particlePath(networkId, []);
const [composeActive, setComposeActive] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
- const statusTab: "open" | "closed" =
- searchParams.get("status") === "closed" ? "closed" : "open";
- const setStatusTab = (next: "open" | "closed") => {
+ const statusTab: 'open' | 'closed' =
+ searchParams.get('status') === 'closed' ? 'closed' : 'open';
+ const setStatusTab = (next: 'open' | 'closed') => {
setSearchParams(
(prev) => {
const params = new URLSearchParams(prev);
- if (next === "open") params.delete("status");
- else params.set("status", next);
+ if (next === 'open') params.delete('status');
+ else params.set('status', next);
return params;
},
{ replace: true },
);
};
- const { streams, isLoading, canLoadMore, loadMore } = useStreamParticles(path, {
- status: statusTab,
- });
+ const { streams, isLoading, canLoadMore, loadMore } = useStreamParticles(
+ path,
+ {
+ status: statusTab,
+ },
+ );
const { selectedIndex } = useStreamKeyboardNav({
streams,
@@ -56,11 +60,17 @@ export default function NetworkRoot() {
setStatusTab(v === "closed" ? "closed" : "open")}
+ onValueChange={(v) =>
+ setStatusTab(v === 'closed' ? 'closed' : 'open')
+ }
>
- Open
- Closed
+
+ Open
+
+
+ Closed
+
@@ -99,39 +109,39 @@ function NetworkRootControls() {
↑↓
- {" "}
+ {' '}
Enter
- {" "}
+ {' '}
navigate
1–9
- {" "}
+ {' '}
jump
requestIntent("record")}
+ onClick={() => requestIntent('record')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Start recording (or hold `)"
>
Hold `
- {" "}
+ {' '}
to start
requestIntent("text")}
+ onClick={() => requestIntent('text')}
className="cursor-pointer rounded transition-colors hover:text-white/80"
title="Compose text (or press T)"
>
T
- {" "}
+ {' '}
text
diff --git a/js/desktop/src/features/network-selector.tsx b/js/desktop/src/features/network-selector.tsx
index 381dfb6..caef58c 100644
--- a/js/desktop/src/features/network-selector.tsx
+++ b/js/desktop/src/features/network-selector.tsx
@@ -1,12 +1,12 @@
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { Check, Plus, Settings, Users } from "lucide-react";
-import { toast } from "sonner";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { Small } from "@/components/ui/typography";
-import { Avatar, AvatarFallback } from "@/components/ui/avatar";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Check, Plus, Settings, Users } from 'lucide-react';
+import { toast } from 'sonner';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { Small } from '@/components/ui/typography';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@@ -14,16 +14,19 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Separator } from "@/components/ui/separator";
-import { useNetworks } from "@/hooks/use-networks";
-import { useMyInvitations, useAcceptInvitation } from "@/hooks/use-member-management";
-import { apiClient } from "@/api/client";
-import { Progress } from "@/components/ui/progress";
-import { toUserMessage } from "@/lib/errors";
-import type { Network, Invitation } from "@/api/types";
+} from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { ScrollArea } from '@/components/ui/scroll-area';
+import { Separator } from '@/components/ui/separator';
+import { useNetworks } from '@/hooks/use-networks';
+import {
+ useMyInvitations,
+ useAcceptInvitation,
+} from '@/hooks/use-member-management';
+import { apiClient } from '@/api/client';
+import { Progress } from '@/components/ui/progress';
+import { toUserMessage } from '@/lib/errors';
+import type { Network, Invitation } from '@/api/types';
function NetworkRow({
network,
@@ -53,7 +56,7 @@ function NetworkRow({