mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
Sort search results by follower count
This commit is contained in:
parent
2b78f4fff2
commit
03fb66156d
5
.changeset/fresh-dolls-jam.md
Normal file
5
.changeset/fresh-dolls-jam.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Sort search results by follower count
|
@ -1,5 +1,5 @@
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
import { RawIncomingNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { RawIncomingNostrEvent, NostrEvent, CountResponse } from "../types/nostr-event";
|
||||
import { NostrOutgoingMessage } from "../types/nostr-query";
|
||||
import { Subject } from "./subject";
|
||||
|
||||
@ -14,6 +14,10 @@ export type IncomingNotice = {
|
||||
message: string;
|
||||
relay: Relay;
|
||||
};
|
||||
export type IncomingCount = {
|
||||
type: "COUNT";
|
||||
relay: Relay;
|
||||
} & CountResponse;
|
||||
export type IncomingEOSE = {
|
||||
type: "EOSE";
|
||||
subId: string;
|
||||
@ -44,6 +48,7 @@ export class Relay {
|
||||
onClose = new Subject<Relay>(undefined, false);
|
||||
onEvent = new Subject<IncomingEvent>(undefined, false);
|
||||
onNotice = new Subject<IncomingNotice>(undefined, false);
|
||||
onCount = new Subject<IncomingCount>(undefined, false);
|
||||
onEOSE = new Subject<IncomingEOSE>(undefined, false);
|
||||
onCommandResult = new Subject<IncomingCommandResult>(undefined, false);
|
||||
ws?: WebSocket;
|
||||
@ -180,6 +185,9 @@ export class Relay {
|
||||
case "NOTICE":
|
||||
this.onNotice.next({ relay: this, type, message: data[1] });
|
||||
break;
|
||||
case "COUNT":
|
||||
this.onCount.next({ relay: this, type, ...data[2] });
|
||||
break;
|
||||
case "EOSE":
|
||||
this.onEOSE.next({ relay: this, type, subId: data[1] });
|
||||
this.endSubResTimer(data[1]);
|
||||
|
@ -1,4 +1,4 @@
|
||||
export type NostrBandProfileStats = {
|
||||
export type NostrBandUserStats = {
|
||||
pubkey: string;
|
||||
pub_note_count: number;
|
||||
pub_post_count: number;
|
||||
@ -57,13 +57,13 @@ export type NostrBandProfileStats = {
|
||||
};
|
||||
};
|
||||
|
||||
class UserTrustedStatsService {
|
||||
private userStats = new Map<string, NostrBandProfileStats>();
|
||||
class TrustedUserStatsService {
|
||||
private userStats = new Map<string, NostrBandUserStats>();
|
||||
|
||||
async fetchUserStats(pubkey: string) {
|
||||
try {
|
||||
const stats = await fetch(`https://api.nostr.band/v0/stats/profile/${pubkey}`).then(
|
||||
(res) => res.json() as Promise<{ stats: Record<string, NostrBandProfileStats> }>,
|
||||
(res) => res.json() as Promise<{ stats: Record<string, NostrBandUserStats> }>,
|
||||
);
|
||||
|
||||
if (stats?.stats[pubkey]) {
|
||||
@ -73,7 +73,7 @@ class UserTrustedStatsService {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
private dedupe = new Map<string, Promise<NostrBandProfileStats | undefined>>();
|
||||
private dedupe = new Map<string, Promise<NostrBandUserStats | undefined>>();
|
||||
async getUserStats(pubkey: string, alwaysFetch = false) {
|
||||
if (this.userStats.has(pubkey) && !alwaysFetch) return this.userStats.get(pubkey)!;
|
||||
|
||||
@ -85,6 +85,6 @@ class UserTrustedStatsService {
|
||||
}
|
||||
}
|
||||
|
||||
const userTrustedStatsService = new UserTrustedStatsService();
|
||||
const trustedUserStatsService = new TrustedUserStatsService();
|
||||
|
||||
export default userTrustedStatsService;
|
||||
export default trustedUserStatsService;
|
@ -15,14 +15,24 @@ export type NostrEvent = {
|
||||
content: string;
|
||||
sig: string;
|
||||
};
|
||||
export type CountResponse = {
|
||||
count: number;
|
||||
approximate?: boolean;
|
||||
};
|
||||
|
||||
export type DraftNostrEvent = Omit<NostrEvent, "pubkey" | "id" | "sig">;
|
||||
|
||||
export type RawIncomingEvent = ["EVENT", string, NostrEvent];
|
||||
export type RawIncomingNotice = ["NOTICE", string];
|
||||
export type RawIncomingCount = ["COUNT", string, CountResponse];
|
||||
export type RawIncomingEOSE = ["EOSE", string];
|
||||
export type RawIncomingCommandResult = ["OK", string, boolean, string];
|
||||
export type RawIncomingNostrEvent = RawIncomingEvent | RawIncomingNotice | RawIncomingEOSE | RawIncomingCommandResult;
|
||||
export type RawIncomingNostrEvent =
|
||||
| RawIncomingEvent
|
||||
| RawIncomingNotice
|
||||
| RawIncomingCount
|
||||
| RawIncomingEOSE
|
||||
| RawIncomingCommandResult;
|
||||
|
||||
export function isETag(tag: Tag): tag is ETag {
|
||||
return tag[0] === "e" && tag[1] !== undefined;
|
||||
|
@ -1,19 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Flex,
|
||||
IconButton,
|
||||
Input,
|
||||
Link,
|
||||
SimpleGrid,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useSearchParams, Link as RouterLink, useNavigate } from "react-router-dom";
|
||||
import { Box, Button, Flex, IconButton, Input, Link, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||
import { useAsync } from "react-use";
|
||||
|
||||
import { ClipboardIcon, QrCodeIcon } from "../../components/icons";
|
||||
import QrScannerModal from "../../components/qr-scanner-modal";
|
||||
import { safeDecode } from "../../helpers/nip19";
|
||||
@ -21,52 +11,46 @@ import { getMatchHashtag } from "../../helpers/regexp";
|
||||
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 { Kind, nip19 } 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 { NostrEvent } from "../../types/nostr-event";
|
||||
import { getUserDisplayName, parseKind0Event } from "../../helpers/user-metadata";
|
||||
import { parseKind0Event } from "../../helpers/user-metadata";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
import { EventRelays } from "../../components/note/note-relays";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
|
||||
import { getEventRelays } from "../../services/event-relays";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import trustedUserStatsService, { NostrBandUserStats } from "../../services/trusted-user-stats";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
|
||||
function buildDescriptionContent(description: string) {
|
||||
let content: EmbedableContent = [description.trim()];
|
||||
function ProfileResult({ profile }: { profile: NostrEvent }) {
|
||||
const metadata = parseKind0Event(profile);
|
||||
|
||||
content = embedNostrLinks(content);
|
||||
content = embedUrls(content, [renderGenericUrl]);
|
||||
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]);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function ProfileResult({ event }: { event: NostrEvent }) {
|
||||
const metadata = parseKind0Event(event);
|
||||
|
||||
const aboutContent = metadata.about && buildDescriptionContent(metadata.about);
|
||||
const nprofile = useMemo(() => {
|
||||
const relays = getEventRelays(event.id).value;
|
||||
const ranked = relayScoreboardService.getRankedRelays(relays).slice(2);
|
||||
return nip19.nprofileEncode({ pubkey: event.pubkey, relays: ranked });
|
||||
}, [event.id]);
|
||||
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(profile.pubkey), [profile.pubkey]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<UserAvatar pubkey={event.pubkey} noProxy mr="2" float="left" />
|
||||
<Link as={RouterLink} to={`/u/${nprofile}`} whiteSpace="nowrap" fontWeight="bold" fontSize="xl" isTruncated>
|
||||
{getUserDisplayName(metadata, event.pubkey)}
|
||||
</Link>
|
||||
<UserAvatar pubkey={profile.pubkey} noProxy mr="2" float="left" />
|
||||
<UserLink pubkey={profile.pubkey} fontWeight="bold" fontSize="xl" isTruncated />
|
||||
<br />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} />
|
||||
<UserDnsIdentityIcon pubkey={profile.pubkey} />
|
||||
<br />
|
||||
<Box whiteSpace="pre" overflow="hidden" maxH="xs">
|
||||
<Box whiteSpace="pre" overflow="hidden" maxH="xs" isTruncated>
|
||||
{aboutContent}
|
||||
</Box>
|
||||
{stats && (
|
||||
<>{stats.followers_pubkey_count && <Text>Followers: {readablizeSats(stats.followers_pubkey_count)}</Text>}</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -81,14 +65,31 @@ function SearchResults({ search }: { search: string }) {
|
||||
{ enabled: !!search },
|
||||
);
|
||||
|
||||
const events = useSubject(timeline?.timeline) ?? [];
|
||||
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}>
|
||||
{events.map((event) => (
|
||||
<ProfileResult key={event.id} event={event} />
|
||||
{sortedProfiles.map((event) => (
|
||||
<ProfileResult key={event.id} profile={event} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
@ -166,7 +167,6 @@ export function SearchPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: remove this when there is a good way to allow the user to select from a list of filtered relays that support NIP-50
|
||||
const searchRelays = ["wss://relay.nostr.band", "wss://search.nos.today", "wss://relay.noswhere.com"];
|
||||
export default function SearchView() {
|
||||
return (
|
||||
|
@ -28,7 +28,7 @@ import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import { getLudEndpoint } from "../../helpers/lnurl";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import { truncatedId } from "../../helpers/nostr/events";
|
||||
import userTrustedStatsService from "../../services/user-trusted-stats";
|
||||
import trustedUserStatsService from "../../services/trusted-user-stats";
|
||||
import { parseAddress } from "../../services/dns-identity";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
@ -66,7 +66,7 @@ export default function UserAboutTab() {
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nprofile = useSharableProfileId(pubkey);
|
||||
|
||||
const { value: stats } = useAsync(() => userTrustedStatsService.getUserStats(pubkey), [pubkey]);
|
||||
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
|
||||
|
||||
const aboutContent = metadata?.about && buildDescriptionContent(metadata?.about);
|
||||
const parsedNip05 = metadata?.nip05 ? parseAddress(metadata.nip05) : undefined;
|
||||
|
Loading…
x
Reference in New Issue
Block a user