mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-04 17:51:12 +02:00
chore: cleanup
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { PublishSpellAction } from "./publish-spell";
|
||||
import accountManager from "@/services/accounts";
|
||||
import pool from "@/services/relay-pool";
|
||||
import publishService from "@/services/publish-service";
|
||||
import * as spellStorage from "@/services/spell-storage";
|
||||
import { LocalSpell } from "@/services/db";
|
||||
|
||||
@@ -15,11 +15,15 @@ vi.mock("@/services/accounts", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/services/relay-pool", () => ({
|
||||
vi.mock("@/services/publish-service", () => ({
|
||||
default: {
|
||||
publish: vi
|
||||
.fn()
|
||||
.mockResolvedValue([{ from: "wss://test.relay/", ok: true }]),
|
||||
publish: vi.fn().mockResolvedValue({
|
||||
publishId: "pub_1",
|
||||
event: {},
|
||||
successful: ["wss://test.relay/"],
|
||||
failed: [],
|
||||
ok: true,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -89,18 +93,18 @@ describe("PublishSpellAction", () => {
|
||||
|
||||
await action.execute(spell);
|
||||
|
||||
// Check if signer was called
|
||||
expect(mockSigner.signEvent).toHaveBeenCalled();
|
||||
|
||||
// Check if published to pool
|
||||
expect(pool.publish).toHaveBeenCalled();
|
||||
// Verify publishService was called (not pool.publish)
|
||||
expect(publishService.publish).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ kind: 777 }),
|
||||
["wss://test.relay/"],
|
||||
);
|
||||
|
||||
// Check if storage updated
|
||||
expect(spellStorage.markSpellPublished).toHaveBeenCalledWith(
|
||||
"local-id",
|
||||
expect.objectContaining({
|
||||
kind: 777,
|
||||
// We expect tags to contain name and alt (description)
|
||||
tags: expect.arrayContaining([
|
||||
["name", "My Spell"],
|
||||
["alt", expect.stringContaining("Description")],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Compact log of relay operations for debugging and introspection.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Check,
|
||||
X,
|
||||
@@ -45,7 +45,6 @@ import { EventErrorBoundary } from "./EventErrorBoundary";
|
||||
|
||||
type TabFilter = "all" | "publish" | "connect" | "auth" | "notice";
|
||||
|
||||
/** Map tab values to the EventLogType(s) they filter */
|
||||
const TAB_TYPE_MAP: Record<TabFilter, EventLogType[] | undefined> = {
|
||||
all: undefined,
|
||||
publish: ["PUBLISH"],
|
||||
@@ -62,6 +61,17 @@ const TAB_FILTERS: { value: TabFilter; label: string }[] = [
|
||||
{ value: "notice", label: "Notice" },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const AUTH_STATUS_TOOLTIP: Record<string, string> = {
|
||||
challenge: "Auth challenge",
|
||||
success: "Auth success",
|
||||
failed: "Auth failed",
|
||||
rejected: "Auth rejected",
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Shared row layout
|
||||
// ============================================================================
|
||||
@@ -284,13 +294,6 @@ function ConnectEntry({ entry }: { entry: ConnectLogEntry }) {
|
||||
function AuthEntry({ entry }: { entry: AuthLogEntry }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const statusTooltip: Record<string, string> = {
|
||||
challenge: "Auth challenge",
|
||||
success: "Auth success",
|
||||
failed: "Auth failed",
|
||||
rejected: "Auth rejected",
|
||||
};
|
||||
|
||||
return (
|
||||
<EntryRow
|
||||
icon={
|
||||
@@ -304,7 +307,7 @@ function AuthEntry({ entry }: { entry: AuthLogEntry }) {
|
||||
<ShieldAlert className="size-3.5 text-muted-foreground" />
|
||||
)
|
||||
}
|
||||
tooltip={statusTooltip[entry.status] ?? "Auth"}
|
||||
tooltip={AUTH_STATUS_TOOLTIP[entry.status] ?? "Auth"}
|
||||
timestamp={entry.timestamp}
|
||||
expanded={expanded}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
@@ -389,6 +392,16 @@ function LogEntryRenderer({
|
||||
// Main Component
|
||||
// ============================================================================
|
||||
|
||||
function getTabCount(
|
||||
tab: TabFilter,
|
||||
totalCount: number,
|
||||
typeCounts: Record<string, number>,
|
||||
): number {
|
||||
const types = TAB_TYPE_MAP[tab];
|
||||
if (!types) return totalCount;
|
||||
return types.reduce((sum, t) => sum + (typeCounts[t] || 0), 0);
|
||||
}
|
||||
|
||||
export function EventLogViewer() {
|
||||
const [activeTab, setActiveTab] = useState<TabFilter>("all");
|
||||
|
||||
@@ -400,33 +413,7 @@ export function EventLogViewer() {
|
||||
retryRelay,
|
||||
totalCount,
|
||||
typeCounts,
|
||||
} = useEventLog({
|
||||
types: filterTypes,
|
||||
});
|
||||
|
||||
/** Get count for a tab filter */
|
||||
const getTabCount = useCallback(
|
||||
(tab: TabFilter): number => {
|
||||
const types = TAB_TYPE_MAP[tab];
|
||||
if (!types) return totalCount;
|
||||
return types.reduce((sum, t) => sum + (typeCounts[t] || 0), 0);
|
||||
},
|
||||
[totalCount, typeCounts],
|
||||
);
|
||||
|
||||
const handleRetry = useCallback(
|
||||
async (entryId: string) => {
|
||||
await retryFailedRelays(entryId);
|
||||
},
|
||||
[retryFailedRelays],
|
||||
);
|
||||
|
||||
const handleRetryRelay = useCallback(
|
||||
async (entryId: string, relay: string) => {
|
||||
await retryRelay(entryId, relay);
|
||||
},
|
||||
[retryRelay],
|
||||
);
|
||||
} = useEventLog({ types: filterTypes });
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -437,20 +424,23 @@ export function EventLogViewer() {
|
||||
onValueChange={(v) => setActiveTab(v as TabFilter)}
|
||||
>
|
||||
<TabsList className="h-7">
|
||||
{TAB_FILTERS.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="text-xs px-1.5 h-5 gap-1"
|
||||
>
|
||||
{tab.label}
|
||||
{getTabCount(tab.value) > 0 && (
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground">
|
||||
{getTabCount(tab.value)}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
{TAB_FILTERS.map((tab) => {
|
||||
const count = getTabCount(tab.value, totalCount, typeCounts);
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="text-xs px-1.5 h-5 gap-1"
|
||||
>
|
||||
{tab.label}
|
||||
{count > 0 && (
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -476,8 +466,8 @@ export function EventLogViewer() {
|
||||
<LogEntryRenderer
|
||||
key={entry.id}
|
||||
entry={entry}
|
||||
onRetry={handleRetry}
|
||||
onRetryRelay={handleRetryRelay}
|
||||
onRetry={retryFailedRelays}
|
||||
onRetryRelay={retryRelay}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -66,6 +66,7 @@ export function EmojiPickerDialog({
|
||||
const [searchResults, setSearchResults] = useState<EmojiSearchResult[]>([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Use the same emoji search hook as chat autocomplete
|
||||
const { service } = useEmojiSearch();
|
||||
@@ -247,7 +248,13 @@ export function EmojiPickerDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-xs p-0 gap-0 overflow-hidden">
|
||||
<DialogContent
|
||||
className="max-w-xs p-0 gap-0 overflow-hidden"
|
||||
onOpenAutoFocus={(e) => {
|
||||
e.preventDefault();
|
||||
searchInputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
{/* Top emojis — recently used quick-picks.
|
||||
This section also provides natural spacing for the dialog close (X) button,
|
||||
which is absolutely positioned at top-right of the dialog. */}
|
||||
@@ -289,13 +296,13 @@ export function EmojiPickerDialog({
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search emojis..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="pl-9"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import eventLog, {
|
||||
type LogEntry,
|
||||
type EventLogType,
|
||||
type PublishLogEntry,
|
||||
} from "@/services/event-log";
|
||||
|
||||
export interface UseEventLogOptions {
|
||||
@@ -23,8 +22,6 @@ export interface UseEventLogOptions {
|
||||
export interface UseEventLogResult {
|
||||
/** Filtered log entries */
|
||||
entries: LogEntry[];
|
||||
/** Publish entries with full status info */
|
||||
publishEntries: PublishLogEntry[];
|
||||
/** Clear all log entries */
|
||||
clear: () => void;
|
||||
/** Retry failed relays for a publish entry */
|
||||
@@ -42,17 +39,9 @@ export interface UseEventLogResult {
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // Get all entries
|
||||
* const { entries } = useEventLog();
|
||||
*
|
||||
* // Filter by type
|
||||
* const { entries } = useEventLog({ types: ["PUBLISH", "CONNECT"] });
|
||||
*
|
||||
* // Filter by relay
|
||||
* const { entries } = useEventLog({ relay: "wss://relay.example.com/" });
|
||||
*
|
||||
* // Limit results
|
||||
* const { entries } = useEventLog({ limit: 50 });
|
||||
* ```
|
||||
*/
|
||||
export function useEventLog(
|
||||
@@ -66,10 +55,7 @@ export function useEventLog(
|
||||
|
||||
// Subscribe to log updates
|
||||
useEffect(() => {
|
||||
const subscription = eventLog.entries$.subscribe((newEntries) => {
|
||||
setEntries(newEntries);
|
||||
});
|
||||
|
||||
const subscription = eventLog.entries$.subscribe(setEntries);
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
@@ -77,17 +63,14 @@ export function useEventLog(
|
||||
const filteredEntries = useMemo(() => {
|
||||
let result = entries;
|
||||
|
||||
// Filter by types
|
||||
if (types && types.length > 0) {
|
||||
result = result.filter((e) => types.includes(e.type));
|
||||
}
|
||||
|
||||
// Filter by relay
|
||||
if (relay) {
|
||||
result = result.filter((e) => e.relay === relay);
|
||||
}
|
||||
|
||||
// Apply limit
|
||||
if (limit && limit > 0) {
|
||||
result = result.slice(0, limit);
|
||||
}
|
||||
@@ -95,41 +78,29 @@ export function useEventLog(
|
||||
return result;
|
||||
}, [entries, types, relay, limit]);
|
||||
|
||||
// Get publish entries
|
||||
const publishEntries = useMemo(() => {
|
||||
return filteredEntries.filter(
|
||||
(e): e is PublishLogEntry => e.type === "PUBLISH",
|
||||
);
|
||||
}, [filteredEntries]);
|
||||
const clear = useCallback(() => eventLog.clear(), []);
|
||||
|
||||
// Clear all entries
|
||||
const clear = useCallback(() => {
|
||||
eventLog.clear();
|
||||
}, []);
|
||||
const retryFailedRelays = useCallback(
|
||||
(entryId: string) => eventLog.retryFailedRelays(entryId),
|
||||
[],
|
||||
);
|
||||
|
||||
// Retry failed relays
|
||||
const retryFailedRelays = useCallback(async (entryId: string) => {
|
||||
await eventLog.retryFailedRelays(entryId);
|
||||
}, []);
|
||||
|
||||
// Retry a single relay
|
||||
const retryRelay = useCallback(async (entryId: string, relay: string) => {
|
||||
await eventLog.retryRelay(entryId, relay);
|
||||
}, []);
|
||||
const retryRelay = useCallback(
|
||||
(entryId: string, relay: string) => eventLog.retryRelay(entryId, relay),
|
||||
[],
|
||||
);
|
||||
|
||||
// Per-type counts from unfiltered entries
|
||||
const typeCounts = useMemo(
|
||||
() =>
|
||||
entries.reduce<Record<string, number>>(
|
||||
(acc, e) => ({ ...acc, [e.type]: (acc[e.type] || 0) + 1 }),
|
||||
{},
|
||||
),
|
||||
[entries],
|
||||
);
|
||||
const typeCounts = useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
for (const e of entries) {
|
||||
counts[e.type] = (counts[e.type] || 0) + 1;
|
||||
}
|
||||
return counts;
|
||||
}, [entries]);
|
||||
|
||||
return {
|
||||
entries: filteredEntries,
|
||||
publishEntries,
|
||||
clear,
|
||||
retryFailedRelays,
|
||||
retryRelay,
|
||||
@@ -137,29 +108,3 @@ export function useEventLog(
|
||||
typeCounts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the latest entry of a specific type
|
||||
*/
|
||||
export function useLatestLogEntry(type: EventLogType): LogEntry | undefined {
|
||||
const { entries } = useEventLog({ types: [type], limit: 1 });
|
||||
return entries[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to subscribe to new log entries as they arrive
|
||||
*/
|
||||
export function useNewLogEntry(
|
||||
callback: (entry: LogEntry) => void,
|
||||
types?: EventLogType[],
|
||||
): void {
|
||||
useEffect(() => {
|
||||
const subscription = eventLog.newEntry$.subscribe((entry) => {
|
||||
if (!types || types.length === 0 || types.includes(entry.type)) {
|
||||
callback(entry);
|
||||
}
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [callback, types]);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const darkTheme: Theme = {
|
||||
mutedForeground: "215 20.2% 70%",
|
||||
|
||||
destructive: "0 72% 63%",
|
||||
destructiveForeground: "0 0% 100%",
|
||||
destructiveForeground: "210 40% 98%",
|
||||
|
||||
border: "217.2 32.6% 17.5%",
|
||||
input: "217.2 32.6% 17.5%",
|
||||
|
||||
@@ -442,29 +442,6 @@ class EventLogService {
|
||||
return [...this.entries];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries filtered by type
|
||||
*/
|
||||
getEntriesByType(type: EventLogType): LogEntry[] {
|
||||
return this.entries.filter((e) => e.type === type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entries for a specific relay
|
||||
*/
|
||||
getEntriesByRelay(relay: string): LogEntry[] {
|
||||
return this.entries.filter((e) => e.relay === relay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publish entries only
|
||||
*/
|
||||
getPublishEntries(): PublishLogEntry[] {
|
||||
return this.entries.filter(
|
||||
(e): e is PublishLogEntry => e.type === "PUBLISH",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entries
|
||||
*/
|
||||
@@ -525,6 +502,3 @@ const eventLog = new EventLogService();
|
||||
eventLog.initialize();
|
||||
|
||||
export default eventLog;
|
||||
|
||||
// Also export the class for testing
|
||||
export { EventLogService };
|
||||
|
||||
@@ -114,13 +114,6 @@ class PublishService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status updates for a specific relay
|
||||
*/
|
||||
getRelayStatusUpdates(relay: string): Observable<RelayStatusUpdate> {
|
||||
return this.status$.pipe(filter((update) => update.relay === relay));
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Publish Methods
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -303,20 +296,6 @@ class PublishService {
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active publish operations
|
||||
*/
|
||||
getActivePublishes(): PublishEvent[] {
|
||||
return Array.from(this.activePublishes.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a publish operation is active
|
||||
*/
|
||||
isPublishing(publishId: string): boolean {
|
||||
return this.activePublishes.has(publishId);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -325,6 +304,3 @@ class PublishService {
|
||||
|
||||
const publishService = new PublishService();
|
||||
export default publishService;
|
||||
|
||||
// Also export the class for testing
|
||||
export { PublishService };
|
||||
|
||||
@@ -617,12 +617,23 @@ export async function selectRelaysForInteraction(
|
||||
authorPubkey: string,
|
||||
targetPubkey: string,
|
||||
): Promise<string[]> {
|
||||
// Fetch relays in parallel
|
||||
const [authorOutbox, targetInbox] = await Promise.all([
|
||||
// Check cache first, only fetch from network if missing
|
||||
const [cachedOutbox, cachedInbox] = await Promise.all([
|
||||
relayListCache.getOutboxRelays(authorPubkey),
|
||||
relayListCache.getInboxRelays(targetPubkey),
|
||||
]);
|
||||
|
||||
const needsFetch: Promise<void>[] = [];
|
||||
if (!cachedOutbox) needsFetch.push(fetchRelayList(authorPubkey, 1000));
|
||||
if (!cachedInbox) needsFetch.push(fetchRelayList(targetPubkey, 1000));
|
||||
if (needsFetch.length > 0) await Promise.all(needsFetch);
|
||||
|
||||
// Re-read after fetch (use cached values if no fetch was needed)
|
||||
const authorOutbox =
|
||||
cachedOutbox ?? (await relayListCache.getOutboxRelays(authorPubkey));
|
||||
const targetInbox =
|
||||
cachedInbox ?? (await relayListCache.getInboxRelays(targetPubkey));
|
||||
|
||||
const outboxRelays = authorOutbox || [];
|
||||
const inboxRelays = targetInbox || [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user