mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 12:49:29 +02:00
add communities to search
This commit is contained in:
parent
313eb19840
commit
6d701a7b24
5
.changeset/new-cobras-draw.md
Normal file
5
.changeset/new-cobras-draw.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to search communities in search view
|
26
src/views/search/article-results.tsx
Normal file
26
src/views/search/article-results.tsx
Normal file
@ -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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<GenericNoteTimeline timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
45
src/views/search/community-results.tsx
Normal file
45
src/views/search/community-results.tsx
Normal file
@ -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<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(community));
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<CommunityCard community={community} maxW="xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{communities.map((community) => (
|
||||
<CommunityResult key={getEventUID(community)} community={community} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Box>
|
||||
<UserAvatar pubkey={profile.pubkey} noProxy mr="2" float="left" />
|
||||
<UserLink pubkey={profile.pubkey} fontWeight="bold" fontSize="xl" isTruncated />
|
||||
<br />
|
||||
<UserDnsIdentityIcon pubkey={profile.pubkey} />
|
||||
<br />
|
||||
<Box whiteSpace="pre" overflow="hidden" maxH="xs" isTruncated>
|
||||
{aboutContent}
|
||||
</Box>
|
||||
{stats && (
|
||||
<>{stats.followers_pubkey_count && <Text>Followers: {readablizeSats(stats.followers_pubkey_count)}</Text>}</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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<Record<string, NostrBandUserStats>>({});
|
||||
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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{sortedProfiles.map((event) => (
|
||||
<ProfileResult key={event.id} profile={event} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<GenericNoteTimeline timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<GenericNoteTimeline timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
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() {
|
||||
</form>
|
||||
|
||||
<Flex gap="2">
|
||||
<ButtonGroup size="sm" isAttached variant="outline">
|
||||
<ButtonGroup size="sm" isAttached variant="outline" flexWrap="wrap">
|
||||
<Button
|
||||
leftIcon={<User01 />}
|
||||
colorScheme={type === "users" ? "primary" : undefined}
|
||||
@ -241,6 +122,13 @@ export function SearchPage() {
|
||||
>
|
||||
Articles
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<CommunityIcon />}
|
||||
colorScheme={type === "communities" ? "primary" : undefined}
|
||||
onClick={() => mergeSearchParams({ type: "communities" })}
|
||||
>
|
||||
Communities
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<RelaySelectionButton ml="auto" size="sm" />
|
||||
</Flex>
|
||||
|
26
src/views/search/note-results.tsx
Normal file
26
src/views/search/note-results.tsx
Normal file
@ -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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<GenericNoteTimeline timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
91
src/views/search/profile-results.tsx
Normal file
91
src/views/search/profile-results.tsx
Normal file
@ -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 (
|
||||
<Box>
|
||||
<UserAvatar pubkey={profile.pubkey} noProxy mr="2" float="left" />
|
||||
<UserLink pubkey={profile.pubkey} fontWeight="bold" fontSize="xl" isTruncated />
|
||||
<br />
|
||||
<UserDnsIdentityIcon pubkey={profile.pubkey} />
|
||||
<br />
|
||||
<Box whiteSpace="pre" overflow="hidden" maxH="xs" isTruncated>
|
||||
{aboutContent}
|
||||
</Box>
|
||||
{stats && (
|
||||
<>{stats.followers_pubkey_count && <Text>Followers: {readablizeSats(stats.followers_pubkey_count)}</Text>}</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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<Record<string, NostrBandUserStats>>({});
|
||||
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 (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{sortedProfiles.map((event) => (
|
||||
<ProfileResult key={event.id} profile={event} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user