- ) : searchQuery.isLoading && results.length === 0 ? (
+ ) : (searchQuery.isLoading || usersQuery.isLoading) &&
+ results.length === 0 ? (
- ) : searchQuery.error instanceof Error && results.length === 0 ? (
+ ) : (searchQuery.error instanceof Error ||
+ usersQuery.error instanceof Error) &&
+ results.length === 0 ? (
-
-
- {channelResults.length +
- (searchQuery.data?.found ?? messageResults.length)}{" "}
- results
-
- Enter to open
-
+
+
+ {sections.map((section) => {
+ let sectionStartIndex = 0;
+ for (const previousSection of sections) {
+ if (previousSection === section) {
+ break;
+ }
+ sectionStartIndex += previousSection.results.length;
+ }
-
- {results.map((result, index) => (
-
openResult(result)}
- onMouseEnter={() => setSelectedIndex(index)}
- testId={resultTestId(result)}
- >
- {result.kind === "channel" ? (
-
- ) : (
-
- )}
-
- ))}
+ return (
+
+
+ {section.title}
+ {section.count}
+
+
+ {section.results.map((result, index) => {
+ const absoluteIndex = sectionStartIndex + index;
+
+ return (
+
+ ) : undefined
+ }
+ onClick={() => openResult(result)}
+ onMouseEnter={() =>
+ setSelectedIndex(absoluteIndex)
+ }
+ testId={resultTestId(result)}
+ >
+ {result.kind === "channel" ? (
+
+ ) : result.kind === "user" ? (
+
+ ) : (
+
+ )}
+
+ );
+ })}
+
+
+ );
+ })}
)}
-
-
- Search is relay-backed and scoped to channels you can access.
-
);
diff --git a/desktop/src/features/search/ui/SearchResultItem.tsx b/desktop/src/features/search/ui/SearchResultItem.tsx
index d520a25c3..f2a75d6cf 100644
--- a/desktop/src/features/search/ui/SearchResultItem.tsx
+++ b/desktop/src/features/search/ui/SearchResultItem.tsx
@@ -1,24 +1,71 @@
import type * as React from "react";
-import { ArrowRight, FileText, Hash, type LucideIcon } from "lucide-react";
+import { FileText, Hash, UserRound, type LucideIcon } from "lucide-react";
+import ReactMarkdown, { type Components } from "react-markdown";
+import remarkBreaks from "remark-breaks";
+import remarkGfm from "remark-gfm";
import {
resolveUserLabel,
resolveUserSecondaryLabel,
+ truncatePubkey,
type UserProfileLookup,
} from "@/features/profile/lib/identity";
-import type { Channel, SearchHit } from "@/shared/api/types";
-import { Badge } from "@/shared/ui/badge";
-import { UserAvatar } from "@/shared/ui/UserAvatar";
+import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
+import type { Channel, SearchHit, UserSearchResult } from "@/shared/api/types";
export type SearchResult =
| { kind: "channel"; channel: Channel }
- | { kind: "message"; hit: SearchHit };
+ | { kind: "message"; hit: SearchHit }
+ | { kind: "user"; user: UserSearchResult };
+
+const INLINE_MARKDOWN_COMPONENTS: Components = {
+ a: ({ children }) =>
{children},
+ blockquote: ({ children }) =>
{children},
+ br: () => " ",
+ code: ({ children }) =>
{children},
+ em: ({ children }) =>
{children},
+ h1: ({ children }) =>
{children},
+ h2: ({ children }) =>
{children},
+ h3: ({ children }) =>
{children},
+ h4: ({ children }) =>
{children},
+ h5: ({ children }) =>
{children},
+ h6: ({ children }) =>
{children},
+ hr: () => null,
+ img: ({ alt, src }) =>
{alt || src || "Image"},
+ li: ({ children }) =>
{children} ,
+ ol: ({ children }) =>
{children},
+ p: ({ children }) =>
{children},
+ pre: ({ children }) =>
{children},
+ strong: ({ children }) =>
{children},
+ table: ({ children }) =>
{children},
+ tbody: ({ children }) =>
{children},
+ td: ({ children }) =>
{children} ,
+ th: ({ children }) =>
{children} ,
+ thead: ({ children }) =>
{children},
+ tr: ({ children }) =>
{children} ,
+ ul: ({ children }) =>
{children},
+};
+
+function InlineMarkdownSnippet({ content }: { content: string }) {
+ return (
+
+ {content}
+
+ );
+}
export function resultKey(result: SearchResult) {
if (result.kind === "channel") {
return `channel-${result.channel.id}`;
}
+ if (result.kind === "user") {
+ return `user-${result.user.pubkey}`;
+ }
+
return `message-${result.hit.eventId}`;
}
@@ -27,6 +74,10 @@ export function resultTestId(result: SearchResult) {
return `search-result-channel-${result.channel.id}`;
}
+ if (result.kind === "user") {
+ return `search-result-user-${result.user.pubkey}`;
+ }
+
return `search-result-${result.hit.eventId}`;
}
@@ -34,6 +85,10 @@ export function resultIcon(
result: SearchResult,
channelLookup: ReadonlyMap
,
) {
+ if (result.kind === "user") {
+ return UserRound;
+ }
+
const channelType =
result.kind === "channel"
? result.channel.channelType
@@ -48,6 +103,7 @@ export function SearchResultShell({
children,
icon: Icon,
isSelected,
+ leading,
onClick,
onMouseEnter,
testId,
@@ -55,6 +111,7 @@ export function SearchResultShell({
children: React.ReactNode;
icon: LucideIcon;
isSelected: boolean;
+ leading?: React.ReactNode;
onClick: () => void;
onMouseEnter: () => void;
testId: string;
@@ -63,46 +120,97 @@ export function SearchResultShell({