feat: isolate crashes, refactor window rendering

This commit is contained in:
Alejandro Gómez
2025-12-11 10:11:20 +01:00
parent 9c9d04c947
commit 36a9b95695
4 changed files with 198 additions and 74 deletions

View File

@@ -3,7 +3,7 @@
## Known Issues
### RTL Support in Rich Text
**Priority**: Medium
**Priority**: Medium
**File**: `src/components/nostr/RichText/Text.tsx`
Current RTL implementation is partial and has limitations:
@@ -11,7 +11,7 @@ Current RTL implementation is partial and has limitations:
- RTL text alignment (right-align) doesn't work properly with inline elements
- Mixed LTR/RTL content with inline elements (hashtags, mentions) creates layout conflicts
**The core problem**:
**The core problem**:
- Inline elements (hashtags, mentions) need inline flow to stay on same line
- RTL alignment requires block-level containers
- These two requirements conflict

View File

@@ -1,21 +1,11 @@
import { useState, useEffect } from "react";
import { useGrimoire } from "@/core/state";
import { useAccountSync } from "@/hooks/useAccountSync";
import Feed from "./nostr/Feed";
import { WinViewer } from "./WinViewer";
import { WindowToolbar } from "./WindowToolbar";
import { TabBar } from "./TabBar";
import { Mosaic, MosaicWindow, MosaicBranch } from "react-mosaic-component";
import { NipRenderer } from "./NipRenderer";
import ManPage from "./ManPage";
import CommandLauncher from "./CommandLauncher";
import ReqViewer from "./ReqViewer";
import { EventDetailViewer } from "./EventDetailViewer";
import { ProfileViewer } from "./ProfileViewer";
import EncodeViewer from "./EncodeViewer";
import DecodeViewer from "./DecodeViewer";
import { RelayViewer } from "./RelayViewer";
import KindRenderer from "./KindRenderer";
import { WindowToolbar } from "./WindowToolbar";
import { WindowTile } from "./WindowTitle";
import { Terminal } from "lucide-react";
import UserMenu from "./nostr/user-menu";
import { GrimoireWelcome } from "./GrimoireWelcome";
@@ -62,68 +52,13 @@ export default function Home() {
);
}
// Render based on appId
let content;
switch (window.appId) {
case "nip":
content = <NipRenderer nipId={window.props.number} />;
break;
case "feed":
content = <Feed className="h-full w-full overflow-auto" />;
break;
case "win":
content = <WinViewer />;
break;
case "kind":
content = <KindRenderer kind={parseInt(window.props.number)} />;
break;
case "man":
content = <ManPage cmd={window.props.cmd} />;
break;
case "req":
content = (
<ReqViewer
filter={window.props.filter}
relays={window.props.relays}
closeOnEose={window.props.closeOnEose}
nip05Authors={window.props.nip05Authors}
nip05PTags={window.props.nip05PTags}
/>
);
break;
case "open":
content = <EventDetailViewer pointer={window.props.pointer} />;
break;
case "profile":
content = <ProfileViewer pubkey={window.props.pubkey} />;
break;
case "encode":
content = <EncodeViewer args={window.props.args} />;
break;
case "decode":
content = <DecodeViewer args={window.props.args} />;
break;
case "relay":
content = <RelayViewer url={window.props.url} />;
break;
default:
content = (
<div className="p-4 text-muted-foreground">
Unknown app: {window.appId}
</div>
);
}
return (
<MosaicWindow
<WindowTile
id={id}
window={window}
path={path}
title={window.title}
toolbarControls={
<WindowToolbar onClose={() => handleRemoveWindow(id)} />
}
>
<div className="h-full w-full overflow-auto">{content}</div>
</MosaicWindow>
onClose={handleRemoveWindow}
/>
);
};

View File

@@ -0,0 +1,166 @@
import { Component, ReactNode } from "react";
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { WindowInstance } from "@/types/app";
import { NipRenderer } from "./NipRenderer";
import ManPage from "./ManPage";
import ReqViewer from "./ReqViewer";
import { EventDetailViewer } from "./EventDetailViewer";
import { ProfileViewer } from "./ProfileViewer";
import EncodeViewer from "./EncodeViewer";
import DecodeViewer from "./DecodeViewer";
import { RelayViewer } from "./RelayViewer";
import KindRenderer from "./KindRenderer";
import Feed from "./nostr/Feed";
import { WinViewer } from "./WinViewer";
interface WindowRendererProps {
window: WindowInstance;
onClose: () => void;
}
interface WindowErrorBoundaryState {
hasError: boolean;
error?: Error;
}
class WindowErrorBoundary extends Component<
{ children: ReactNode; windowTitle: string; onClose: () => void },
WindowErrorBoundaryState
> {
constructor(props: {
children: ReactNode;
windowTitle: string;
onClose: () => void;
}) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): WindowErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error(
`Window "${this.props.windowTitle}" crashed:`,
error,
errorInfo,
);
}
render() {
if (this.state.hasError) {
return (
<div className="p-4">
<div className="border border-red-500 bg-red-50 dark:bg-red-950 rounded-md p-4">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 space-y-2">
<h3 className="font-semibold text-red-900 dark:text-red-100">
Window Crashed
</h3>
<p className="text-sm text-red-800 dark:text-red-200">
{this.state.error?.message ||
"An unexpected error occurred in this window."}
</p>
<Button
variant="outline"
size="sm"
onClick={this.props.onClose}
className="mt-2"
>
Close Window
</Button>
</div>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export function WindowRenderer({ window, onClose }: WindowRendererProps) {
let content: ReactNode;
try {
switch (window.appId) {
case "nip":
content = <NipRenderer nipId={window.props.number} />;
break;
case "feed":
content = <Feed className="h-full w-full overflow-auto" />;
break;
case "win":
content = <WinViewer />;
break;
case "kind":
content = <KindRenderer kind={parseInt(window.props.number)} />;
break;
case "man":
content = <ManPage cmd={window.props.cmd} />;
break;
case "req":
content = (
<ReqViewer
filter={window.props.filter}
relays={window.props.relays}
closeOnEose={window.props.closeOnEose}
nip05Authors={window.props.nip05Authors}
nip05PTags={window.props.nip05PTags}
/>
);
break;
case "open":
content = <EventDetailViewer pointer={window.props.pointer} />;
break;
case "profile":
content = <ProfileViewer pubkey={window.props.pubkey} />;
break;
case "encode":
content = <EncodeViewer args={window.props.args} />;
break;
case "decode":
content = <DecodeViewer args={window.props.args} />;
break;
case "relay":
content = <RelayViewer url={window.props.url} />;
break;
default:
content = (
<div className="p-4 text-muted-foreground">
Unknown app: {window.appId}
</div>
);
}
} catch (error) {
content = (
<div className="p-4">
<div className="border border-red-500 bg-red-50 dark:bg-red-950 rounded-md p-4">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-red-600 dark:text-red-400" />
<div className="flex-1">
<h3 className="font-semibold text-red-900 dark:text-red-100">
Failed to render window
</h3>
<p className="text-sm text-red-800 dark:text-red-200 mt-1">
{error instanceof Error
? error.message
: "An unexpected error occurred"}
</p>
</div>
</div>
</div>
</div>
);
}
return (
<WindowErrorBoundary windowTitle={window.title} onClose={onClose}>
<div className="h-full w-full overflow-auto">{content}</div>
</WindowErrorBoundary>
);
}

View File

@@ -0,0 +1,23 @@
import { MosaicWindow, MosaicBranch } from "react-mosaic-component";
import { WindowInstance } from "@/types/app";
import { WindowToolbar } from "./WindowToolbar";
import { WindowRenderer } from "./WindowRenderer";
interface WindowTileProps {
id: string;
window: WindowInstance;
path: MosaicBranch[];
onClose: (id: string) => void;
}
export function WindowTile({ id, window, path, onClose }: WindowTileProps) {
return (
<MosaicWindow
path={path}
title={window.title}
toolbarControls={<WindowToolbar onClose={() => onClose(id)} />}
>
<WindowRenderer window={window} onClose={() => onClose(id)} />
</MosaicWindow>
);
}