diff --git a/.changeset/new-cobras-draw.md b/.changeset/new-cobras-draw.md
new file mode 100644
index 000000000..9e8ec1be6
--- /dev/null
+++ b/.changeset/new-cobras-draw.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add option to search communities in search view
diff --git a/src/views/search/article-results.tsx b/src/views/search/article-results.tsx
new file mode 100644
index 000000000..6b313ff28
--- /dev/null
+++ b/src/views/search/article-results.tsx
@@ -0,0 +1,26 @@
+import { Kind } from "nostr-tools";
+
+import { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import IntersectionObserverProvider from "../../providers/intersection-observer";
+import GenericNoteTimeline from "../../components/timeline-page/generic-note-timeline";
+
+export default function ArticleSearchResults({ search }: { search: string }) {
+ const searchRelays = useRelaySelectionRelays();
+
+ const timeline = useTimelineLoader(
+ `${search}-article-search`,
+ searchRelays,
+ { search: search || "", kinds: [Kind.Article] },
+ { enabled: !!search },
+ );
+
+ const callback = useTimelineCurserIntersectionCallback(timeline);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/views/search/community-results.tsx b/src/views/search/community-results.tsx
new file mode 100644
index 000000000..acf5caf90
--- /dev/null
+++ b/src/views/search/community-results.tsx
@@ -0,0 +1,45 @@
+import { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
+import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
+import { useRef } from "react";
+import { getEventUID } from "../../helpers/nostr/events";
+import { NostrEvent } from "../../types/nostr-event";
+import CommunityCard from "../communities/components/community-card";
+import useSubject from "../../hooks/use-subject";
+
+function CommunityResult({ community }: { community: NostrEvent }) {
+ const ref = useRef(null);
+ useRegisterIntersectionEntity(ref, getEventUID(community));
+
+ return (
+
+
+
+ );
+}
+
+export default function CommunitySearchResults({ search }: { search: string }) {
+ const searchRelays = useRelaySelectionRelays();
+
+ const timeline = useTimelineLoader(
+ `${search}-community-search`,
+ searchRelays,
+ { search: search || "", kinds: [COMMUNITY_DEFINITION_KIND] },
+ { enabled: !!search },
+ );
+
+ const communities = useSubject(timeline.timeline);
+ const callback = useTimelineCurserIntersectionCallback(timeline);
+
+ return (
+
+
+ {communities.map((community) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/views/search/index.tsx b/src/views/search/index.tsx
index 5f6cec9f3..43f3970e7 100644
--- a/src/views/search/index.tsx
+++ b/src/views/search/index.tsx
@@ -1,144 +1,22 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { Box, Button, ButtonGroup, Flex, IconButton, Input, Link, Text, useDisclosure } from "@chakra-ui/react";
-import { Kind } from "nostr-tools";
+import { useCallback, useEffect, useState } from "react";
+import { Button, ButtonGroup, Flex, IconButton, Input, Link, useDisclosure } from "@chakra-ui/react";
import { useSearchParams, useNavigate } from "react-router-dom";
-import { useAsync } from "react-use";
import { SEARCH_RELAYS } from "../../const";
-import { NostrEvent } from "../../types/nostr-event";
import { safeDecode } from "../../helpers/nip19";
import { getMatchHashtag } from "../../helpers/regexp";
-import { parseKind0Event } from "../../helpers/user-metadata";
-import { readablizeSats } from "../../helpers/bolt11";
import { searchParamsToJson } from "../../helpers/url";
-import { EmbedableContent, embedUrls } from "../../helpers/embeds";
-import { CopyToClipboardIcon, NotesIcon, QrCodeIcon } from "../../components/icons";
+import { CommunityIcon, CopyToClipboardIcon, NotesIcon, QrCodeIcon } from "../../components/icons";
import QrScannerModal from "../../components/qr-scanner-modal";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
-import RelaySelectionProvider, { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
-import useTimelineLoader from "../../hooks/use-timeline-loader";
-import useSubject from "../../hooks/use-subject";
-import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
-import IntersectionObserverProvider from "../../providers/intersection-observer";
-import UserAvatar from "../../components/user-avatar";
-import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
-import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
-import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
-import { UserLink } from "../../components/user-link";
-import trustedUserStatsService, { NostrBandUserStats } from "../../services/trusted-user-stats";
+import RelaySelectionProvider from "../../providers/relay-selection-provider";
import VerticalPageLayout from "../../components/vertical-page-layout";
import User01 from "../../components/icons/user-01";
-import GenericNoteTimeline from "../../components/timeline-page/generic-note-timeline";
import Feather from "../../components/icons/feather";
-
-function ProfileResult({ profile }: { profile: NostrEvent }) {
- const metadata = parseKind0Event(profile);
-
- const aboutContent = useMemo(() => {
- if (!metadata.about) return null;
- let content: EmbedableContent = [metadata.about.trim()];
- content = embedNostrLinks(content);
- content = embedUrls(content, [renderGenericUrl]);
- return content;
- }, [metadata.about]);
-
- const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(profile.pubkey), [profile.pubkey]);
-
- return (
-
-
-
-
-
-
-
- {aboutContent}
-
- {stats && (
- <>{stats.followers_pubkey_count && Followers: {readablizeSats(stats.followers_pubkey_count)}}>
- )}
-
- );
-}
-
-function ProfileSearchResults({ search }: { search: string }) {
- const searchRelays = useRelaySelectionRelays();
-
- const timeline = useTimelineLoader(
- `${search}-profile-search`,
- searchRelays,
- { search: search || "", kinds: [Kind.Metadata] },
- { enabled: !!search },
- );
-
- const profiles = useSubject(timeline?.timeline) ?? [];
-
- const [profileStats, setProfileStats] = useState>({});
- useEffect(() => {
- for (const profile of profiles) {
- trustedUserStatsService.getUserStats(profile.pubkey).then((stats) => {
- if (!stats) return;
- setProfileStats((dir) => ({ ...dir, [stats.pubkey]: stats }));
- });
- }
- }, [profiles]);
-
- const sortedProfiles = useMemo(() => {
- return profiles.sort(
- (a, b) =>
- (profileStats[b.pubkey]?.followers_pubkey_count ?? 0) - (profileStats[a.pubkey]?.followers_pubkey_count ?? 0),
- );
- }, [profileStats, profiles]);
-
- const callback = useTimelineCurserIntersectionCallback(timeline);
-
- return (
-
- {sortedProfiles.map((event) => (
-
- ))}
-
-
- );
-}
-
-function NoteSearchResults({ search }: { search: string }) {
- const searchRelays = useRelaySelectionRelays();
-
- const timeline = useTimelineLoader(
- `${search}-note-search`,
- searchRelays,
- { search: search || "", kinds: [Kind.Text] },
- { enabled: !!search },
- );
-
- const callback = useTimelineCurserIntersectionCallback(timeline);
-
- return (
-
-
-
- );
-}
-
-function ArticleSearchResults({ search }: { search: string }) {
- const searchRelays = useRelaySelectionRelays();
-
- const timeline = useTimelineLoader(
- `${search}-article-search`,
- searchRelays,
- { search: search || "", kinds: [Kind.Article] },
- { enabled: !!search },
- );
-
- const callback = useTimelineCurserIntersectionCallback(timeline);
-
- return (
-
-
-
- );
-}
+import ProfileSearchResults from "./profile-results";
+import NoteSearchResults from "./note-results";
+import ArticleSearchResults from "./article-results";
+import CommunitySearchResults from "./community-results";
export function SearchPage() {
const navigate = useNavigate();
@@ -199,6 +77,9 @@ export function SearchPage() {
case "articles":
SearchResults = ArticleSearchResults;
break;
+ case "communities":
+ SearchResults = CommunitySearchResults;
+ break;
}
return (
@@ -219,7 +100,7 @@ export function SearchPage() {
-
+
}
colorScheme={type === "users" ? "primary" : undefined}
@@ -241,6 +122,13 @@ export function SearchPage() {
>
Articles
+ }
+ colorScheme={type === "communities" ? "primary" : undefined}
+ onClick={() => mergeSearchParams({ type: "communities" })}
+ >
+ Communities
+
diff --git a/src/views/search/note-results.tsx b/src/views/search/note-results.tsx
new file mode 100644
index 000000000..4155e7615
--- /dev/null
+++ b/src/views/search/note-results.tsx
@@ -0,0 +1,26 @@
+import { Kind } from "nostr-tools";
+
+import { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import IntersectionObserverProvider from "../../providers/intersection-observer";
+import GenericNoteTimeline from "../../components/timeline-page/generic-note-timeline";
+
+export default function NoteSearchResults({ search }: { search: string }) {
+ const searchRelays = useRelaySelectionRelays();
+
+ const timeline = useTimelineLoader(
+ `${search}-note-search`,
+ searchRelays,
+ { search: search || "", kinds: [Kind.Text] },
+ { enabled: !!search },
+ );
+
+ const callback = useTimelineCurserIntersectionCallback(timeline);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/views/search/profile-results.tsx b/src/views/search/profile-results.tsx
new file mode 100644
index 000000000..04ae896f6
--- /dev/null
+++ b/src/views/search/profile-results.tsx
@@ -0,0 +1,91 @@
+import { useEffect, useMemo, useState } from "react";
+import { Box, Text } from "@chakra-ui/react";
+import { useAsync } from "react-use";
+
+import { NostrEvent } from "../../types/nostr-event";
+import { parseKind0Event } from "../../helpers/user-metadata";
+import { readablizeSats } from "../../helpers/bolt11";
+import { EmbedableContent, embedUrls } from "../../helpers/embeds";
+import UserAvatar from "../../components/user-avatar";
+import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
+import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
+import { UserLink } from "../../components/user-link";
+import trustedUserStatsService, { NostrBandUserStats } from "../../services/trusted-user-stats";
+import { useRelaySelectionRelays } from "../../providers/relay-selection-provider";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import { Kind } from "nostr-tools";
+import useSubject from "../../hooks/use-subject";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import IntersectionObserverProvider from "../../providers/intersection-observer";
+import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
+
+function ProfileResult({ profile }: { profile: NostrEvent }) {
+ const metadata = parseKind0Event(profile);
+
+ const aboutContent = useMemo(() => {
+ if (!metadata.about) return null;
+ let content: EmbedableContent = [metadata.about.trim()];
+ content = embedNostrLinks(content);
+ content = embedUrls(content, [renderGenericUrl]);
+ return content;
+ }, [metadata.about]);
+
+ const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(profile.pubkey), [profile.pubkey]);
+
+ return (
+
+
+
+
+
+
+
+ {aboutContent}
+
+ {stats && (
+ <>{stats.followers_pubkey_count && Followers: {readablizeSats(stats.followers_pubkey_count)}}>
+ )}
+
+ );
+}
+
+export default function ProfileSearchResults({ search }: { search: string }) {
+ const searchRelays = useRelaySelectionRelays();
+
+ const timeline = useTimelineLoader(
+ `${search}-profile-search`,
+ searchRelays,
+ { search: search || "", kinds: [Kind.Metadata] },
+ { enabled: !!search },
+ );
+
+ const profiles = useSubject(timeline?.timeline) ?? [];
+
+ const [profileStats, setProfileStats] = useState>({});
+ useEffect(() => {
+ for (const profile of profiles) {
+ trustedUserStatsService.getUserStats(profile.pubkey).then((stats) => {
+ if (!stats) return;
+ setProfileStats((dir) => ({ ...dir, [stats.pubkey]: stats }));
+ });
+ }
+ }, [profiles]);
+
+ const sortedProfiles = useMemo(() => {
+ return profiles.sort(
+ (a, b) =>
+ (profileStats[b.pubkey]?.followers_pubkey_count ?? 0) - (profileStats[a.pubkey]?.followers_pubkey_count ?? 0),
+ );
+ }, [profileStats, profiles]);
+
+ const callback = useTimelineCurserIntersectionCallback(timeline);
+
+ return (
+
+ {sortedProfiles.map((event) => (
+
+ ))}
+
+
+ );
+}