chore: cleanup

This commit is contained in:
Alejandro Gómez
2026-03-04 17:12:35 +01:00
parent 26bb0713ac
commit 498007401d
8 changed files with 97 additions and 190 deletions

View File

@@ -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")],

View File

@@ -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}
/>
))
)}

View File

@@ -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>

View File

@@ -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]);
}

View File

@@ -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%",

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 || [];