mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-18 03:17:04 +02:00
Refactor the chat composer into a reusable NostrEditor component with configurable behavior for different UI contexts (chat, posts, long-form). Key changes: - Create NostrEditor with configurable props: - submitBehavior: 'enter' | 'ctrl-enter' | 'button-only' - variant: 'inline' | 'multiline' | 'full' - blobPreview: 'compact' | 'card' | 'gallery' - Extract suggestion system into pluggable architecture (SuggestionConfig) - Add helper functions to create standard Nostr suggestions - Update search hooks (useProfileSearch, useEmojiSearch) with injectable sources for custom profile/emoji sets - Convert MentionEditor to backward-compatible wrapper around NostrEditor - Update ChatViewer to use new NostrEditor component This enables building WYSIWYG editors with the same autocomplete features as chat (profile mentions, emoji, uploads) but with different behaviors suitable for long-form posts or notes.
118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { useEffect, useMemo, useRef } from "react";
|
|
import type { Observable } from "rxjs";
|
|
import {
|
|
ProfileSearchService,
|
|
type ProfileSearchResult,
|
|
} from "@/services/profile-search";
|
|
import eventStore from "@/services/event-store";
|
|
import type { NostrEvent } from "@/types/nostr";
|
|
|
|
export interface UseProfileSearchOptions {
|
|
/** Initial profiles to index immediately */
|
|
initialProfiles?: NostrEvent[];
|
|
/** Custom observable source for profiles (replaces default EventStore subscription) */
|
|
profileSource$?: Observable<NostrEvent[]>;
|
|
/** Whether to also include profiles from global EventStore (default: true) */
|
|
includeGlobal?: boolean;
|
|
/** Maximum results to return (default: 20) */
|
|
limit?: number;
|
|
}
|
|
|
|
/**
|
|
* Hook to provide profile search functionality with automatic indexing
|
|
* of profiles from the event store.
|
|
*
|
|
* Supports injectable sources for custom profile sets (e.g., group members only).
|
|
*
|
|
* @example
|
|
* // Default: index all profiles from global EventStore
|
|
* const { searchProfiles } = useProfileSearch();
|
|
*
|
|
* @example
|
|
* // Custom source: only group members
|
|
* const { searchProfiles } = useProfileSearch({
|
|
* profileSource$: groupMemberProfiles$,
|
|
* includeGlobal: false,
|
|
* });
|
|
*
|
|
* @example
|
|
* // Pre-populate with known profiles
|
|
* const { searchProfiles } = useProfileSearch({
|
|
* initialProfiles: knownProfiles,
|
|
* });
|
|
*/
|
|
export function useProfileSearch(options: UseProfileSearchOptions = {}) {
|
|
const {
|
|
initialProfiles,
|
|
profileSource$,
|
|
includeGlobal = true,
|
|
limit = 20,
|
|
} = options;
|
|
|
|
const serviceRef = useRef<ProfileSearchService | null>(null);
|
|
|
|
// Create service instance (singleton per component mount)
|
|
if (!serviceRef.current) {
|
|
serviceRef.current = new ProfileSearchService();
|
|
// Index initial profiles immediately if provided
|
|
if (initialProfiles && initialProfiles.length > 0) {
|
|
serviceRef.current.addProfiles(initialProfiles);
|
|
}
|
|
}
|
|
|
|
const service = serviceRef.current;
|
|
|
|
// Subscribe to custom profile source if provided
|
|
useEffect(() => {
|
|
if (!profileSource$) return;
|
|
|
|
const subscription = profileSource$.subscribe({
|
|
next: (events) => {
|
|
service.addProfiles(events);
|
|
},
|
|
error: (error) => {
|
|
console.error("Failed to load profiles from custom source:", error);
|
|
},
|
|
});
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
};
|
|
}, [profileSource$, service]);
|
|
|
|
// Subscribe to global profile events from the event store
|
|
useEffect(() => {
|
|
if (!includeGlobal) return;
|
|
|
|
const subscription = eventStore
|
|
.timeline([{ kinds: [0], limit: 1000 }])
|
|
.subscribe({
|
|
next: (events) => {
|
|
service.addProfiles(events);
|
|
},
|
|
error: (error) => {
|
|
console.error("Failed to load profiles for search:", error);
|
|
},
|
|
});
|
|
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
service.clear(); // Clean up indexed profiles
|
|
};
|
|
}, [service, includeGlobal]);
|
|
|
|
// Memoize search function
|
|
const searchProfiles = useMemo(
|
|
() =>
|
|
async (query: string): Promise<ProfileSearchResult[]> => {
|
|
return await service.search(query, { limit });
|
|
},
|
|
[service, limit],
|
|
);
|
|
|
|
return {
|
|
searchProfiles,
|
|
service,
|
|
};
|
|
}
|