mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
feat: timestamps and user locale
This commit is contained in:
@@ -175,7 +175,7 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) {
|
||||
) : event.kind === 9802 ? (
|
||||
<Kind9802DetailRenderer event={event} />
|
||||
) : (
|
||||
<KindRenderer event={event} showTimestamp={true} />
|
||||
<KindRenderer event={event} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ export function JsonViewer({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-auto mt-2">
|
||||
<pre className="text-xs font-mono bg-muted p-4 rounded-lg">
|
||||
<pre className="text-xs font-mono bg-muted p-4 overflow-scroll">
|
||||
{jsonString}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
import { Menu, Copy, FileJson, ExternalLink } from "lucide-react";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { JsonViewer } from "@/components/JsonViewer";
|
||||
import { formatTimestamp } from "@/hooks/useLocale";
|
||||
|
||||
/**
|
||||
* Universal event properties and utilities shared across all kind renderers
|
||||
*/
|
||||
export interface BaseEventProps {
|
||||
event: NostrEvent;
|
||||
showTimestamp?: boolean;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
@@ -126,35 +126,37 @@ export function EventMenu({ event }: { event: NostrEvent }) {
|
||||
* Base event container with universal header
|
||||
* Kind-specific renderers can wrap their content with this
|
||||
*/
|
||||
/**
|
||||
* Format relative time (e.g., "2m ago", "3h ago", "5d ago")
|
||||
*/
|
||||
|
||||
export function BaseEventContainer({
|
||||
event,
|
||||
children,
|
||||
showTimestamp = false,
|
||||
}: {
|
||||
event: NostrEvent;
|
||||
children: React.ReactNode;
|
||||
showTimestamp?: boolean;
|
||||
}) {
|
||||
// Format timestamp
|
||||
const timestamp = new Date(event.created_at * 1000).toLocaleString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
// Format relative time for display
|
||||
const { locale } = useGrimoire();
|
||||
const relativeTime = formatTimestamp(event.created_at, "relative", locale.locale);
|
||||
|
||||
// Format absolute timestamp for hover (ISO-8601 style)
|
||||
const absoluteTime = formatTimestamp(event.created_at, "absolute", locale.locale);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-3 border-b border-border/50 last:border-0">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<EventAuthor pubkey={event.pubkey} />
|
||||
{showTimestamp ? (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{timestamp}
|
||||
<div className="flex flex-row gap-2 items-baseline">
|
||||
<EventAuthor pubkey={event.pubkey} />
|
||||
<span
|
||||
className="text-xs font-light text-muted-foreground cursor-help"
|
||||
title={absoluteTime}
|
||||
>
|
||||
{relativeTime}
|
||||
</span>
|
||||
) : (
|
||||
<EventMenu event={event} />
|
||||
)}
|
||||
</div>
|
||||
<EventMenu event={event} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RichText } from "../RichText";
|
||||
* Renderer for Kind 0 - Profile Metadata
|
||||
* Displays as a compact profile card in feed view
|
||||
*/
|
||||
export function Kind0Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind0Renderer({ event }: BaseEventProps) {
|
||||
const pubkey = event.pubkey;
|
||||
const profile = useProfile(pubkey);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Kind0Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
const website = profile?.website;
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Profile Info */}
|
||||
<div className="flex flex-col gap-2 p-3 border border-muted bg-muted/20">
|
||||
|
||||
@@ -14,7 +14,7 @@ import { FileText, Download } from "lucide-react";
|
||||
* Renderer for Kind 1063 - File Metadata (NIP-94)
|
||||
* Displays file metadata with appropriate preview for images, videos, and audio
|
||||
*/
|
||||
export function Kind1063Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind1063Renderer({ event }: BaseEventProps) {
|
||||
const metadata = parseFileMetadata(event);
|
||||
|
||||
// Determine file type from MIME
|
||||
@@ -29,7 +29,7 @@ export function Kind1063Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
event.tags.find((t) => t[0] === "summary")?.[1] || event.content;
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File preview */}
|
||||
{metadata.url && (isImage || isVideo || isAudio) ? (
|
||||
|
||||
@@ -9,24 +9,16 @@ import { useGrimoire } from "@/core/state";
|
||||
/**
|
||||
* Renderer for Kind 1 - Short Text Note
|
||||
*/
|
||||
export function Kind1Renderer({
|
||||
event,
|
||||
showTimestamp,
|
||||
depth = 0,
|
||||
}: BaseEventProps) {
|
||||
export function Kind1Renderer({ event, depth = 0 }: BaseEventProps) {
|
||||
const { addWindow } = useGrimoire();
|
||||
const refs = getNip10References(event);
|
||||
const hasReply = refs.reply?.e || refs.reply?.a;
|
||||
|
||||
// Fetch parent event if replying
|
||||
const parentEvent = useNostrEvent(
|
||||
hasReply ? refs.reply?.e || refs.reply?.a : undefined,
|
||||
);
|
||||
const pointer =
|
||||
refs.reply?.e || refs.reply?.a || refs.root?.e || refs.root?.a;
|
||||
const parentEvent = useNostrEvent(pointer);
|
||||
|
||||
const handleReplyClick = () => {
|
||||
if (!parentEvent) return;
|
||||
|
||||
const pointer = refs.reply?.e || refs.reply?.a;
|
||||
if (pointer) {
|
||||
addWindow(
|
||||
"open",
|
||||
@@ -37,8 +29,8 @@ export function Kind1Renderer({
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
{hasReply && parentEvent && (
|
||||
<BaseEventContainer event={event}>
|
||||
{pointer && parentEvent && (
|
||||
<div
|
||||
onClick={handleReplyClick}
|
||||
className="flex items-start gap-2 p-1 bg-muted/20 text-xs text-muted-foreground hover:bg-muted/30 cursor-pointer rounded transition-colors"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { parseImetaTags } from "@/lib/imeta";
|
||||
* Renderer for Kind 20 - Picture Event (NIP-68)
|
||||
* Picture-first feed events with imeta tags for image metadata
|
||||
*/
|
||||
export function Kind20Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind20Renderer({ event }: BaseEventProps) {
|
||||
// Parse imeta tags to get image URLs and metadata
|
||||
const images = parseImetaTags(event);
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Kind20Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
const title = event.tags.find((t) => t[0] === "title")?.[1];
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Title if present */}
|
||||
{title && (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { parseImetaTags } from "@/lib/imeta";
|
||||
* Renderer for Kind 21 - Video Event (NIP-71)
|
||||
* Horizontal/landscape video events with imeta tags
|
||||
*/
|
||||
export function Kind21Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind21Renderer({ event }: BaseEventProps) {
|
||||
// Parse imeta tags to get video URLs and metadata
|
||||
const videos = parseImetaTags(event);
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Kind21Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
const title = event.tags.find((t) => t[0] === "title")?.[1];
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Title if present */}
|
||||
{title && <h3 className="text-base font-semibold">{title}</h3>}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { parseImetaTags } from "@/lib/imeta";
|
||||
* Renderer for Kind 22 - Short Video Event (NIP-71)
|
||||
* Short-form portrait video events (like TikTok/Reels)
|
||||
*/
|
||||
export function Kind22Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind22Renderer({ event }: BaseEventProps) {
|
||||
// Parse imeta tags to get video URLs and metadata
|
||||
const videos = parseImetaTags(event);
|
||||
|
||||
@@ -15,7 +15,7 @@ export function Kind22Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
const title = event.tags.find((t) => t[0] === "title")?.[1];
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Title if present */}
|
||||
{title && <h3 className="text-base font-semibold">{title}</h3>}
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
* Renderer for Kind 30023 - Long-form Article
|
||||
* Displays article title and summary in feed
|
||||
*/
|
||||
export function Kind30023Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind30023Renderer({ event }: BaseEventProps) {
|
||||
const title = useMemo(() => getArticleTitle(event), [event]);
|
||||
const summary = useMemo(() => getArticleSummary(event), [event]);
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Title */}
|
||||
{title && (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Users, Sparkles } from "lucide-react";
|
||||
* Kind 3 Renderer - Contact/Follow List
|
||||
* Shows follow count and "follows you" indicator
|
||||
*/
|
||||
export function Kind3Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind3Renderer({ event }: BaseEventProps) {
|
||||
const { state } = useGrimoire();
|
||||
|
||||
// Extract followed pubkeys from p tags
|
||||
@@ -20,7 +20,7 @@ export function Kind3Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
: false;
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2 text-xs">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="size-3 text-muted-foreground" />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useGrimoire } from "@/core/state";
|
||||
* Renderer for Kind 6 - Reposts
|
||||
* Displays repost indicator with the original event embedded
|
||||
*/
|
||||
export function Kind6Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind6Renderer({ event }: BaseEventProps) {
|
||||
const { addWindow } = useGrimoire();
|
||||
|
||||
// Get the event being reposted (e tag)
|
||||
@@ -15,7 +15,7 @@ export function Kind6Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
const repostedEventId = eTag?.[1];
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Repeat2 className="size-4" />
|
||||
|
||||
@@ -10,7 +10,7 @@ import { KindRenderer } from "./index";
|
||||
* Displays emoji/reaction with the event being reacted to
|
||||
* Supports both e tags (event ID) and a tags (address/replaceable events)
|
||||
*/
|
||||
export function Kind7Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind7Renderer({ event }: BaseEventProps) {
|
||||
// Get the reaction content (usually an emoji)
|
||||
const reaction = event.content || "❤️";
|
||||
|
||||
@@ -108,7 +108,7 @@ export function Kind7Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Reaction indicator */}
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -18,7 +18,7 @@ import { RichText } from "../RichText";
|
||||
* Renderer for Kind 9735 - Zap Receipts
|
||||
* Displays zap amount, sender, and zapped content
|
||||
*/
|
||||
export function Kind9735Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind9735Renderer({ event }: BaseEventProps) {
|
||||
// Validate zap
|
||||
const isValid = useMemo(() => isValidZap(event), [event]);
|
||||
|
||||
@@ -49,7 +49,7 @@ export function Kind9735Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
|
||||
if (!isValid) {
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="text-xs text-muted-foreground">Invalid zap receipt</div>
|
||||
</BaseEventContainer>
|
||||
);
|
||||
@@ -65,7 +65,7 @@ export function Kind9735Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={displayEvent} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={displayEvent} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Zap indicator */}
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
* Renderer for Kind 9802 - Highlight
|
||||
* Displays highlighted text with optional comment and source URL
|
||||
*/
|
||||
export function Kind9802Renderer({ event, showTimestamp }: BaseEventProps) {
|
||||
export function Kind9802Renderer({ event }: BaseEventProps) {
|
||||
const highlightText = useMemo(() => getHighlightText(event), [event]);
|
||||
const sourceUrl = useMemo(() => getHighlightSourceUrl(event), [event]);
|
||||
const comment = useMemo(() => getHighlightComment(event), [event]);
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event} >
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Comment */}
|
||||
{comment && <p className="text-sm text-foreground">{comment}</p>}
|
||||
|
||||
@@ -37,9 +37,9 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
* Default renderer for kinds without custom implementations
|
||||
* Shows basic event info with raw content
|
||||
*/
|
||||
function DefaultKindRenderer({ event, showTimestamp }: BaseEventProps) {
|
||||
function DefaultKindRenderer({ event }: BaseEventProps) {
|
||||
return (
|
||||
<BaseEventContainer event={event} showTimestamp={showTimestamp}>
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="text-xs mb-1">Kind {event.kind} event</div>
|
||||
<pre className="text-xs overflow-x-auto whitespace-pre-wrap break-words">
|
||||
@@ -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 <Renderer event={event} showTimestamp={showTimestamp} depth={depth} />;
|
||||
return <Renderer event={event} depth={depth} />;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<GrimoireState>(
|
||||
// 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;
|
||||
|
||||
113
src/hooks/useLocale.ts
Normal file
113
src/hooks/useLocale.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -46,4 +46,11 @@ export interface GrimoireState {
|
||||
pubkey: string;
|
||||
relays?: UserRelays;
|
||||
};
|
||||
locale?: {
|
||||
locale: string;
|
||||
language: string;
|
||||
region?: string;
|
||||
timezone: string;
|
||||
timeFormat: "12h" | "24h";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"}
|
||||
{"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"}
|
||||
Reference in New Issue
Block a user