mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 10:11:12 +02:00
feat: isolate crashes, refactor window rendering
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -3,7 +3,7 @@
|
|||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
### RTL Support in Rich Text
|
### RTL Support in Rich Text
|
||||||
**Priority**: Medium
|
**Priority**: Medium
|
||||||
**File**: `src/components/nostr/RichText/Text.tsx`
|
**File**: `src/components/nostr/RichText/Text.tsx`
|
||||||
|
|
||||||
Current RTL implementation is partial and has limitations:
|
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
|
- RTL text alignment (right-align) doesn't work properly with inline elements
|
||||||
- Mixed LTR/RTL content with inline elements (hashtags, mentions) creates layout conflicts
|
- 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
|
- Inline elements (hashtags, mentions) need inline flow to stay on same line
|
||||||
- RTL alignment requires block-level containers
|
- RTL alignment requires block-level containers
|
||||||
- These two requirements conflict
|
- These two requirements conflict
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useGrimoire } from "@/core/state";
|
import { useGrimoire } from "@/core/state";
|
||||||
import { useAccountSync } from "@/hooks/useAccountSync";
|
import { useAccountSync } from "@/hooks/useAccountSync";
|
||||||
import Feed from "./nostr/Feed";
|
|
||||||
import { WinViewer } from "./WinViewer";
|
|
||||||
import { WindowToolbar } from "./WindowToolbar";
|
|
||||||
import { TabBar } from "./TabBar";
|
import { TabBar } from "./TabBar";
|
||||||
import { Mosaic, MosaicWindow, MosaicBranch } from "react-mosaic-component";
|
import { Mosaic, MosaicWindow, MosaicBranch } from "react-mosaic-component";
|
||||||
import { NipRenderer } from "./NipRenderer";
|
|
||||||
import ManPage from "./ManPage";
|
|
||||||
import CommandLauncher from "./CommandLauncher";
|
import CommandLauncher from "./CommandLauncher";
|
||||||
import ReqViewer from "./ReqViewer";
|
import { WindowToolbar } from "./WindowToolbar";
|
||||||
import { EventDetailViewer } from "./EventDetailViewer";
|
import { WindowTile } from "./WindowTitle";
|
||||||
import { ProfileViewer } from "./ProfileViewer";
|
|
||||||
import EncodeViewer from "./EncodeViewer";
|
|
||||||
import DecodeViewer from "./DecodeViewer";
|
|
||||||
import { RelayViewer } from "./RelayViewer";
|
|
||||||
import KindRenderer from "./KindRenderer";
|
|
||||||
import { Terminal } from "lucide-react";
|
import { Terminal } from "lucide-react";
|
||||||
import UserMenu from "./nostr/user-menu";
|
import UserMenu from "./nostr/user-menu";
|
||||||
import { GrimoireWelcome } from "./GrimoireWelcome";
|
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 (
|
return (
|
||||||
<MosaicWindow
|
<WindowTile
|
||||||
|
id={id}
|
||||||
|
window={window}
|
||||||
path={path}
|
path={path}
|
||||||
title={window.title}
|
onClose={handleRemoveWindow}
|
||||||
toolbarControls={
|
/>
|
||||||
<WindowToolbar onClose={() => handleRemoveWindow(id)} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="h-full w-full overflow-auto">{content}</div>
|
|
||||||
</MosaicWindow>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
166
src/components/WindowRenderer.tsx
Normal file
166
src/components/WindowRenderer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/WindowTitle.tsx
Normal file
23
src/components/WindowTitle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user