diff --git a/src/components/EventDetailViewer.tsx b/src/components/EventDetailViewer.tsx
index 219ad81..c02c1e9 100644
--- a/src/components/EventDetailViewer.tsx
+++ b/src/components/EventDetailViewer.tsx
@@ -175,7 +175,7 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
) : event.kind === 9802 ? (
+
{jsonString}
{comment}
} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index caba42f..c0ddc0a 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -37,9 +37,9 @@ const kindRenderers: Record
@@ -56,15 +56,13 @@ function DefaultKindRenderer({ event, showTimestamp }: BaseEventProps) {
*/
export function KindRenderer({
event,
- showTimestamp = false,
depth = 0,
}: {
event: NostrEvent;
- showTimestamp?: boolean;
depth?: number;
}) {
const Renderer = kindRenderers[event.kind] || DefaultKindRenderer;
- return ;
+ return ;
}
/**
diff --git a/src/core/state.ts b/src/core/state.ts
index 7d4adec..5b26db1 100644
--- a/src/core/state.ts
+++ b/src/core/state.ts
@@ -1,6 +1,7 @@
import { useAtom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { GrimoireState, AppId } from "@/types/app";
+import { useLocale } from "@/hooks/useLocale";
import * as Logic from "./logic";
// Initial State Definition - Empty canvas on first load
@@ -26,9 +27,16 @@ export const grimoireStateAtom = atomWithStorage(
// The Hook
export const useGrimoire = () => {
const [state, setState] = useAtom(grimoireStateAtom);
+ const browserLocale = useLocale();
+
+ // Initialize locale from browser if not set
+ if (!state.locale) {
+ setState((prev) => ({ ...prev, locale: browserLocale }));
+ }
return {
state,
+ locale: state.locale || browserLocale,
activeWorkspace: state.workspaces[state.activeWorkspaceId],
createWorkspace: () => {
const count = Object.keys(state.workspaces).length + 1;
diff --git a/src/hooks/useLocale.ts b/src/hooks/useLocale.ts
new file mode 100644
index 0000000..f8dcf34
--- /dev/null
+++ b/src/hooks/useLocale.ts
@@ -0,0 +1,113 @@
+import { useMemo } from "react";
+
+export interface LocaleConfig {
+ /** Browser's detected locale (e.g., 'en-US', 'pt-BR', 'ja-JP') */
+ locale: string;
+ /** Language code (e.g., 'en', 'pt', 'ja') */
+ language: string;
+ /** Region code (e.g., 'US', 'BR', 'JP') */
+ region?: string;
+ /** Timezone (e.g., 'America/New_York') */
+ timezone: string;
+ /** 12h or 24h time preference */
+ timeFormat: "12h" | "24h";
+}
+
+/**
+ * Hook to get user's locale preferences from browser
+ * Falls back to en-US if detection fails
+ */
+export function useLocale(): LocaleConfig {
+ return useMemo(() => {
+ // Get browser locale
+ const browserLocale =
+ navigator.language || navigator.languages?.[0] || "en-US";
+
+ // Parse locale into language and region
+ const [language, region] = browserLocale.split("-");
+
+ // Detect timezone
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+
+ // Detect 12h vs 24h preference by formatting a test date
+ const testDate = new Date(2000, 0, 1, 13, 0); // 1PM
+ const formatted = testDate.toLocaleTimeString(browserLocale, {
+ hour: "numeric",
+ });
+ const timeFormat = formatted.includes("PM") || formatted.includes("AM") ? "12h" : "24h";
+
+ return {
+ locale: browserLocale,
+ language,
+ region,
+ timezone,
+ timeFormat,
+ };
+ }, []);
+}
+
+/**
+ * Format a timestamp according to locale preferences
+ * @param timestamp - Unix timestamp in seconds
+ * @param style - 'relative' for "2h ago", 'absolute' for full date/time, 'date' for date only, 'time' for time only
+ */
+export function formatTimestamp(
+ timestamp: number,
+ style: "relative" | "absolute" | "date" | "time" = "relative",
+ locale?: string,
+): string {
+ const browserLocale = locale || navigator.language || "en-US";
+ const date = new Date(timestamp * 1000);
+
+ if (style === "relative") {
+ const now = Date.now();
+ const diff = now - timestamp * 1000;
+ const seconds = Math.floor(diff / 1000);
+ const minutes = Math.floor(seconds / 60);
+ const hours = Math.floor(minutes / 60);
+ const days = Math.floor(hours / 24);
+ const weeks = Math.floor(days / 7);
+ const months = Math.floor(days / 30);
+ const years = Math.floor(days / 365);
+
+ if (seconds < 60) return `${seconds}s ago`;
+ if (minutes < 60) return `${minutes}m ago`;
+ if (hours < 24) return `${hours}h ago`;
+ if (days < 7) return `${days}d ago`;
+ if (weeks < 4) return `${weeks}w ago`;
+ if (months < 12) return `${months}mo ago`;
+ return `${years}y ago`;
+ }
+
+ if (style === "absolute") {
+ // ISO-8601 style: 2025-12-10 23:42
+ return date
+ .toLocaleString(browserLocale, {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ })
+ .replace(",", "");
+ }
+
+ if (style === "date") {
+ return date.toLocaleDateString(browserLocale, {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ });
+ }
+
+ if (style === "time") {
+ return date.toLocaleTimeString(browserLocale, {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ });
+ }
+
+ return date.toLocaleString(browserLocale);
+}
diff --git a/src/types/app.ts b/src/types/app.ts
index 3d7141a..8b8d24d 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -46,4 +46,11 @@ export interface GrimoireState {
pubkey: string;
relays?: UserRelays;
};
+ locale?: {
+ locale: string;
+ language: string;
+ region?: string;
+ timezone: string;
+ timeFormat: "12h" | "24h";
+ };
}
diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo
index cee3c77..0b65815 100644
--- a/tsconfig.app.tsbuildinfo
+++ b/tsconfig.app.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/main.tsx","./src/root.tsx","./src/vite-env.d.ts","./src/components/command.tsx","./src/components/commandlauncher.tsx","./src/components/decodeviewer.tsx","./src/components/encodeviewer.tsx","./src/components/eventdetailviewer.tsx","./src/components/grimoirewelcome.tsx","./src/components/home.tsx","./src/components/jsonviewer.tsx","./src/components/kindbadge.tsx","./src/components/kindrenderer.tsx","./src/components/manpage.tsx","./src/components/markdown.tsx","./src/components/niprenderer.tsx","./src/components/profileviewer.tsx","./src/components/reqviewer.tsx","./src/components/tabbar.tsx","./src/components/timestamp.tsx","./src/components/winviewer.tsx","./src/components/windowtoolbar.tsx","./src/components/nostr/embeddedevent.tsx","./src/components/nostr/feed.tsx","./src/components/nostr/mediadialog.tsx","./src/components/nostr/mediaembed.tsx","./src/components/nostr/quotedevent.tsx","./src/components/nostr/richtext.tsx","./src/components/nostr/username.tsx","./src/components/nostr/index.ts","./src/components/nostr/nip05.tsx","./src/components/nostr/npub.tsx","./src/components/nostr/relay-pool.tsx","./src/components/nostr/user-menu.tsx","./src/components/nostr/linkpreview/audiolink.tsx","./src/components/nostr/linkpreview/imagelink.tsx","./src/components/nostr/linkpreview/plainlink.tsx","./src/components/nostr/linkpreview/videolink.tsx","./src/components/nostr/linkpreview/index.ts","./src/components/nostr/richtext/emoji.tsx","./src/components/nostr/richtext/eventembed.tsx","./src/components/nostr/richtext/gallery.tsx","./src/components/nostr/richtext/hashtag.tsx","./src/components/nostr/richtext/link.tsx","./src/components/nostr/richtext/mention.tsx","./src/components/nostr/richtext/text.tsx","./src/components/nostr/richtext/index.ts","./src/components/nostr/kinds/baseeventrenderer.tsx","./src/components/nostr/kinds/kind0detailrenderer.tsx","./src/components/nostr/kinds/kind0renderer.tsx","./src/components/nostr/kinds/kind1063renderer.tsx","./src/components/nostr/kinds/kind1renderer.tsx","./src/components/nostr/kinds/kind20renderer.tsx","./src/components/nostr/kinds/kind21renderer.tsx","./src/components/nostr/kinds/kind22renderer.tsx","./src/components/nostr/kinds/kind30023detailrenderer.tsx","./src/components/nostr/kinds/kind30023renderer.tsx","./src/components/nostr/kinds/kind3renderer.tsx","./src/components/nostr/kinds/kind6renderer.tsx","./src/components/nostr/kinds/kind7renderer.tsx","./src/components/nostr/kinds/kind9735renderer.tsx","./src/components/nostr/kinds/kind9802detailrenderer.tsx","./src/components/nostr/kinds/kind9802renderer.tsx","./src/components/nostr/kinds/index.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/constants/kinds.ts","./src/constants/nips.ts","./src/core/logic.ts","./src/core/state.ts","./src/hooks/useaccountsync.ts","./src/hooks/usenip.ts","./src/hooks/usenip05.ts","./src/hooks/usenostrevent.ts","./src/hooks/useprofile.ts","./src/hooks/usereqtimeline.ts","./src/hooks/usetimeline.ts","./src/lib/decode-parser.ts","./src/lib/encode-parser.ts","./src/lib/imeta.ts","./src/lib/nip-kinds.ts","./src/lib/nip05.ts","./src/lib/nostr-utils.ts","./src/lib/open-parser.ts","./src/lib/profile-parser.ts","./src/lib/req-parser.ts","./src/lib/utils.ts","./src/services/accounts.ts","./src/services/db.ts","./src/services/event-store.ts","./src/services/loaders.ts","./src/services/relay-pool.ts","./src/types/app.ts","./src/types/man.ts","./src/types/nostr.ts","./src/types/profile.ts"],"version":"5.6.3"}
\ No newline at end of file
+{"root":["./src/main.tsx","./src/root.tsx","./src/vite-env.d.ts","./src/components/command.tsx","./src/components/commandlauncher.tsx","./src/components/decodeviewer.tsx","./src/components/encodeviewer.tsx","./src/components/eventdetailviewer.tsx","./src/components/grimoirewelcome.tsx","./src/components/home.tsx","./src/components/jsonviewer.tsx","./src/components/kindbadge.tsx","./src/components/kindrenderer.tsx","./src/components/manpage.tsx","./src/components/markdown.tsx","./src/components/niprenderer.tsx","./src/components/profileviewer.tsx","./src/components/reqviewer.tsx","./src/components/tabbar.tsx","./src/components/timestamp.tsx","./src/components/winviewer.tsx","./src/components/windowtoolbar.tsx","./src/components/nostr/embeddedevent.tsx","./src/components/nostr/feed.tsx","./src/components/nostr/mediadialog.tsx","./src/components/nostr/mediaembed.tsx","./src/components/nostr/quotedevent.tsx","./src/components/nostr/richtext.tsx","./src/components/nostr/username.tsx","./src/components/nostr/index.ts","./src/components/nostr/nip05.tsx","./src/components/nostr/npub.tsx","./src/components/nostr/relay-pool.tsx","./src/components/nostr/user-menu.tsx","./src/components/nostr/linkpreview/audiolink.tsx","./src/components/nostr/linkpreview/imagelink.tsx","./src/components/nostr/linkpreview/plainlink.tsx","./src/components/nostr/linkpreview/videolink.tsx","./src/components/nostr/linkpreview/index.ts","./src/components/nostr/richtext/emoji.tsx","./src/components/nostr/richtext/eventembed.tsx","./src/components/nostr/richtext/gallery.tsx","./src/components/nostr/richtext/hashtag.tsx","./src/components/nostr/richtext/link.tsx","./src/components/nostr/richtext/mention.tsx","./src/components/nostr/richtext/text.tsx","./src/components/nostr/richtext/index.ts","./src/components/nostr/kinds/baseeventrenderer.tsx","./src/components/nostr/kinds/kind0detailrenderer.tsx","./src/components/nostr/kinds/kind0renderer.tsx","./src/components/nostr/kinds/kind1063renderer.tsx","./src/components/nostr/kinds/kind1renderer.tsx","./src/components/nostr/kinds/kind20renderer.tsx","./src/components/nostr/kinds/kind21renderer.tsx","./src/components/nostr/kinds/kind22renderer.tsx","./src/components/nostr/kinds/kind30023detailrenderer.tsx","./src/components/nostr/kinds/kind30023renderer.tsx","./src/components/nostr/kinds/kind3renderer.tsx","./src/components/nostr/kinds/kind6renderer.tsx","./src/components/nostr/kinds/kind7renderer.tsx","./src/components/nostr/kinds/kind9735renderer.tsx","./src/components/nostr/kinds/kind9802detailrenderer.tsx","./src/components/nostr/kinds/kind9802renderer.tsx","./src/components/nostr/kinds/index.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/constants/kinds.ts","./src/constants/nips.ts","./src/core/logic.ts","./src/core/state.ts","./src/hooks/useaccountsync.ts","./src/hooks/uselocale.ts","./src/hooks/usenip.ts","./src/hooks/usenip05.ts","./src/hooks/usenostrevent.ts","./src/hooks/useprofile.ts","./src/hooks/usereqtimeline.ts","./src/hooks/usetimeline.ts","./src/lib/decode-parser.ts","./src/lib/encode-parser.ts","./src/lib/imeta.ts","./src/lib/nip-kinds.ts","./src/lib/nip05.ts","./src/lib/nostr-utils.ts","./src/lib/open-parser.ts","./src/lib/profile-parser.ts","./src/lib/req-parser.ts","./src/lib/utils.ts","./src/services/accounts.ts","./src/services/db.ts","./src/services/event-store.ts","./src/services/loaders.ts","./src/services/relay-pool.ts","./src/types/app.ts","./src/types/man.ts","./src/types/nostr.ts","./src/types/profile.ts"],"version":"5.6.3"}
\ No newline at end of file