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() { - + + 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) => ( + + ))} + + + ); +}