mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-08 20:08:02 +02:00
show repost counts on notes
hide goals, streams, and articles tabs in user view if user has none
This commit is contained in:
parent
c635b2bc4b
commit
5a455c7d49
5
.changeset/dry-buckets-mix.md
Normal file
5
.changeset/dry-buckets-mix.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Show repost counts on notes
|
@ -34,6 +34,7 @@
|
|||||||
"hls.js": "^1.4.10",
|
"hls.js": "^1.4.10",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"identicon.js": "^2.3.3",
|
"identicon.js": "^2.3.3",
|
||||||
|
"json-stringify-deterministic": "^1.0.11",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet.locatecontrol": "^0.79.0",
|
"leaflet.locatecontrol": "^0.79.0",
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { CountResponse, NostrEvent } from "../types/nostr-event";
|
||||||
import { NostrRequestFilter } from "../types/nostr-query";
|
import { NostrRequestFilter } from "../types/nostr-query";
|
||||||
import relayPoolService from "../services/relay-pool";
|
import relayPoolService from "../services/relay-pool";
|
||||||
import Relay, { IncomingEOSE, IncomingEvent } from "./relay";
|
import Relay, { IncomingCount, IncomingEOSE, IncomingEvent } from "./relay";
|
||||||
import Subject from "./subject";
|
import Subject from "./subject";
|
||||||
import createDefer from "./deferred";
|
import createDefer from "./deferred";
|
||||||
|
|
||||||
@ -16,7 +16,8 @@ export default class NostrRequest {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
relays: Set<Relay>;
|
relays: Set<Relay>;
|
||||||
state = NostrRequest.IDLE;
|
state = NostrRequest.IDLE;
|
||||||
onEvent = new Subject<NostrEvent>();
|
onEvent = new Subject<NostrEvent>(undefined, false);
|
||||||
|
onCount = new Subject<CountResponse>(undefined, false);
|
||||||
onComplete = createDefer<void>();
|
onComplete = createDefer<void>();
|
||||||
seenEvents = new Set<string>();
|
seenEvents = new Set<string>();
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ export default class NostrRequest {
|
|||||||
for (const relay of this.relays) {
|
for (const relay of this.relays) {
|
||||||
relay.onEOSE.subscribe(this.handleEOSE, this);
|
relay.onEOSE.subscribe(this.handleEOSE, this);
|
||||||
relay.onEvent.subscribe(this.handleEvent, this);
|
relay.onEvent.subscribe(this.handleEvent, this);
|
||||||
|
relay.onCount.subscribe(this.handleCount, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
|
this.timeout = timeout ?? REQUEST_DEFAULT_TIMEOUT;
|
||||||
@ -57,8 +59,13 @@ export default class NostrRequest {
|
|||||||
this.seenEvents.add(incomingEvent.body.id);
|
this.seenEvents.add(incomingEvent.body.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
handleCount(incomingCount: IncomingCount) {
|
||||||
|
if (incomingCount.subId === this.id) {
|
||||||
|
this.onCount.next({ count: incomingCount.count, approximate: incomingCount.approximate });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
start(filter: NostrRequestFilter) {
|
start(filter: NostrRequestFilter, type: "REQ" | "COUNT" = "REQ") {
|
||||||
if (this.state !== NostrRequest.IDLE) {
|
if (this.state !== NostrRequest.IDLE) {
|
||||||
throw new Error("cant restart a nostr request");
|
throw new Error("cant restart a nostr request");
|
||||||
}
|
}
|
||||||
@ -66,8 +73,8 @@ export default class NostrRequest {
|
|||||||
this.state = NostrRequest.RUNNING;
|
this.state = NostrRequest.RUNNING;
|
||||||
for (const relay of this.relays) {
|
for (const relay of this.relays) {
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
relay.send(["REQ", this.id, ...filter]);
|
relay.send([type, this.id, ...filter]);
|
||||||
} else relay.send(["REQ", this.id, filter]);
|
} else relay.send([type, this.id, filter]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => this.complete(), this.timeout);
|
setTimeout(() => this.complete(), this.timeout);
|
||||||
|
@ -16,6 +16,7 @@ export type IncomingNotice = {
|
|||||||
};
|
};
|
||||||
export type IncomingCount = {
|
export type IncomingCount = {
|
||||||
type: "COUNT";
|
type: "COUNT";
|
||||||
|
subId: string;
|
||||||
relay: Relay;
|
relay: Relay;
|
||||||
} & CountResponse;
|
} & CountResponse;
|
||||||
export type IncomingEOSE = {
|
export type IncomingEOSE = {
|
||||||
@ -116,7 +117,7 @@ export default class Relay {
|
|||||||
this.ws?.send(JSON.stringify(json));
|
this.ws?.send(JSON.stringify(json));
|
||||||
|
|
||||||
// record start time
|
// record start time
|
||||||
if (json[0] === "REQ") {
|
if (json[0] === "REQ" || json[0] === "COUNT") {
|
||||||
this.startSubResTimer(json[1]);
|
this.startSubResTimer(json[1]);
|
||||||
}
|
}
|
||||||
} else this.queue.push(json);
|
} else this.queue.push(json);
|
||||||
@ -186,7 +187,7 @@ export default class Relay {
|
|||||||
this.onNotice.next({ relay: this, type, message: data[1] });
|
this.onNotice.next({ relay: this, type, message: data[1] });
|
||||||
break;
|
break;
|
||||||
case "COUNT":
|
case "COUNT":
|
||||||
this.onCount.next({ relay: this, type, ...data[2] });
|
this.onCount.next({ relay: this, type, subId: data[1], ...data[2] });
|
||||||
break;
|
break;
|
||||||
case "EOSE":
|
case "EOSE":
|
||||||
this.onEOSE.next({ relay: this, type, subId: data[1] });
|
this.onEOSE.next({ relay: this, type, subId: data[1] });
|
||||||
|
@ -116,7 +116,7 @@ export default function PayStep({ callbacks, onComplete }: { callbacks: PayReque
|
|||||||
{callbacks.map(({ pubkey, invoice, error }) => {
|
{callbacks.map(({ pubkey, invoice, error }) => {
|
||||||
if (paid.includes(pubkey))
|
if (paid.includes(pubkey))
|
||||||
return (
|
return (
|
||||||
<UserCard pubkey={pubkey}>
|
<UserCard key={pubkey} pubkey={pubkey}>
|
||||||
<Button size="sm" variant="outline" colorScheme="green" leftIcon={<CheckIcon />}>
|
<Button size="sm" variant="outline" colorScheme="green" leftIcon={<CheckIcon />}>
|
||||||
Paid
|
Paid
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -23,6 +23,7 @@ import { useSigningContext } from "../../../providers/signing-provider";
|
|||||||
import { EmbedEvent } from "../../embed-event";
|
import { EmbedEvent } from "../../embed-event";
|
||||||
import relayScoreboardService from "../../../services/relay-scoreboard";
|
import relayScoreboardService from "../../../services/relay-scoreboard";
|
||||||
import { getEventRelays } from "../../../services/event-relays";
|
import { getEventRelays } from "../../../services/event-relays";
|
||||||
|
import useEventCount from "../../../hooks/use-event-count";
|
||||||
|
|
||||||
function buildRepost(event: NostrEvent): DraftNostrEvent {
|
function buildRepost(event: NostrEvent): DraftNostrEvent {
|
||||||
const relays = getEventRelays(event.id).value;
|
const relays = getEventRelays(event.id).value;
|
||||||
@ -45,6 +46,8 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
|
|
||||||
|
const repostCount = useEventCount({ "#e": [event.id], kinds: [6] });
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -61,13 +64,19 @@ export function RepostButton({ event }: { event: NostrEvent }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
{repostCount !== undefined && repostCount > 0 ? (
|
||||||
icon={<RepostIcon />}
|
<Button leftIcon={<RepostIcon />} onClick={onOpen} title="Repost Note" isLoading={loading}>
|
||||||
onClick={onOpen}
|
{repostCount}
|
||||||
aria-label="Repost Note"
|
</Button>
|
||||||
title="Repost Note"
|
) : (
|
||||||
isLoading={loading}
|
<IconButton
|
||||||
/>
|
icon={<RepostIcon />}
|
||||||
|
onClick={onOpen}
|
||||||
|
aria-label="Repost Note"
|
||||||
|
title="Repost Note"
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
|
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
|
10
src/hooks/use-event-count.ts
Normal file
10
src/hooks/use-event-count.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import eventCountService from "../services/event-count";
|
||||||
|
import { NostrRequestFilter } from "../types/nostr-query";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
|
export default function useEventCount(filter?: NostrRequestFilter, alwaysRequest = false) {
|
||||||
|
const key = filter ? eventCountService.stringifyFilter(filter) : "empty";
|
||||||
|
const subject = useMemo(() => filter && eventCountService.requestCount(filter, alwaysRequest), [key, alwaysRequest]);
|
||||||
|
return useSubject(subject);
|
||||||
|
}
|
5
src/hooks/use-user-event-kind-count.ts
Normal file
5
src/hooks/use-user-event-kind-count.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import useEventCount from "./use-event-count";
|
||||||
|
|
||||||
|
export default function useUserEventKindCount(pubkey: string, kind: number) {
|
||||||
|
return useEventCount({ authors: [pubkey], kinds: [kind] });
|
||||||
|
}
|
44
src/services/event-count.ts
Normal file
44
src/services/event-count.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import stringify from "json-stringify-deterministic";
|
||||||
|
import Subject from "../classes/subject";
|
||||||
|
import SuperMap from "../classes/super-map";
|
||||||
|
import { NostrRequestFilter } from "../types/nostr-query";
|
||||||
|
import NostrRequest from "../classes/nostr-request";
|
||||||
|
|
||||||
|
// TODO: move this to settings
|
||||||
|
const COUNT_RELAY = "wss://relay.nostr.band";
|
||||||
|
|
||||||
|
class EventCountService {
|
||||||
|
subjects = new SuperMap<string, Subject<number>>(() => new Subject<number>());
|
||||||
|
|
||||||
|
stringifyFilter(filter: NostrRequestFilter) {
|
||||||
|
return stringify(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCount(filter: NostrRequestFilter, alwaysRequest = false) {
|
||||||
|
const key = this.stringifyFilter(filter);
|
||||||
|
const sub = this.subjects.get(key);
|
||||||
|
|
||||||
|
if (sub.value === undefined || alwaysRequest) {
|
||||||
|
const request = new NostrRequest([COUNT_RELAY]);
|
||||||
|
request.onCount.subscribe((c) => sub.next(c.count));
|
||||||
|
request.start(filter, "COUNT");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCount(filter: NostrRequestFilter) {
|
||||||
|
const key = this.stringifyFilter(filter);
|
||||||
|
const sub = this.subjects.get(key);
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventCountService = new EventCountService();
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.eventCountService = eventCountService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default eventCountService;
|
@ -2,9 +2,10 @@ import { NostrEvent } from "./nostr-event";
|
|||||||
|
|
||||||
export type NostrOutgoingEvent = ["EVENT", NostrEvent];
|
export type NostrOutgoingEvent = ["EVENT", NostrEvent];
|
||||||
export type NostrOutgoingRequest = ["REQ", string, ...NostrQuery[]];
|
export type NostrOutgoingRequest = ["REQ", string, ...NostrQuery[]];
|
||||||
|
export type NostrOutgoingCount = ["COUNT", string, ...NostrQuery[]];
|
||||||
export type NostrOutgoingClose = ["CLOSE", string];
|
export type NostrOutgoingClose = ["CLOSE", string];
|
||||||
|
|
||||||
export type NostrOutgoingMessage = NostrOutgoingEvent | NostrOutgoingRequest | NostrOutgoingClose;
|
export type NostrOutgoingMessage = NostrOutgoingEvent | NostrOutgoingRequest | NostrOutgoingClose | NostrOutgoingCount;
|
||||||
|
|
||||||
export type NostrQuery = {
|
export type NostrQuery = {
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
|
@ -29,17 +29,21 @@ import { useUserMetadata } from "../../hooks/use-user-metadata";
|
|||||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||||
import { isHexKey } from "../../helpers/nip19";
|
import { isHexKey } from "../../helpers/nip19";
|
||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
import { Suspense, useState } from "react";
|
import { Suspense, useMemo, useState } from "react";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||||
import { RelayMode } from "../../classes/relay";
|
import { RelayMode } from "../../classes/relay";
|
||||||
import { AdditionalRelayProvider } from "../../providers/additional-relay-context";
|
import { AdditionalRelayProvider } from "../../providers/additional-relay-context";
|
||||||
import { nip19 } from "nostr-tools";
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
import { unique } from "../../helpers/array";
|
import { unique } from "../../helpers/array";
|
||||||
import { RelayFavicon } from "../../components/relay-favicon";
|
import { RelayFavicon } from "../../components/relay-favicon";
|
||||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
import { useUserRelays } from "../../hooks/use-user-relays";
|
||||||
import Header from "./components/header";
|
import Header from "./components/header";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
|
import useUserEventKindCount from "../../hooks/use-user-event-kind-count";
|
||||||
|
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||||
|
import { GOAL_KIND } from "../../helpers/nostr/goal";
|
||||||
|
import { useMeasure } from "react-use";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: "About", path: "about" },
|
{ label: "About", path: "about" },
|
||||||
@ -94,10 +98,27 @@ const UserView = () => {
|
|||||||
const userTopRelays = useUserTopRelays(pubkey, relayCount);
|
const userTopRelays = useUserTopRelays(pubkey, relayCount);
|
||||||
const relayModal = useDisclosure();
|
const relayModal = useDisclosure();
|
||||||
|
|
||||||
|
const articleCount = useUserEventKindCount(pubkey, Kind.Article);
|
||||||
|
const streamCount = useUserEventKindCount(pubkey, STREAM_KIND);
|
||||||
|
const goalCount = useUserEventKindCount(pubkey, GOAL_KIND);
|
||||||
|
|
||||||
|
const filteredTabs = useMemo(
|
||||||
|
() =>
|
||||||
|
tabs.filter((t) => {
|
||||||
|
if (t.path === "streams" && streamCount === 0) return false;
|
||||||
|
if (t.path === "goals" && goalCount === 0) return false;
|
||||||
|
if (t.path === "articles" && articleCount === 0) return false;
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
[streamCount, goalCount, articleCount],
|
||||||
|
);
|
||||||
|
|
||||||
const matches = useMatches();
|
const matches = useMatches();
|
||||||
const lastMatch = matches[matches.length - 1];
|
const lastMatch = matches[matches.length - 1];
|
||||||
|
|
||||||
const activeTab = tabs.indexOf(tabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? tabs[0]);
|
const activeTab = filteredTabs.indexOf(
|
||||||
|
filteredTabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? filteredTabs[0],
|
||||||
|
);
|
||||||
|
|
||||||
const metadata = useUserMetadata(pubkey, userTopRelays, { alwaysRequest: true });
|
const metadata = useUserMetadata(pubkey, userTopRelays, { alwaysRequest: true });
|
||||||
|
|
||||||
@ -114,12 +135,12 @@ const UserView = () => {
|
|||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
isLazy
|
isLazy
|
||||||
index={activeTab}
|
index={activeTab}
|
||||||
onChange={(v) => navigate(tabs[v].path, { replace: true })}
|
onChange={(v) => navigate(filteredTabs[v].path, { replace: true })}
|
||||||
colorScheme="primary"
|
colorScheme="primary"
|
||||||
h="full"
|
h="full"
|
||||||
>
|
>
|
||||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||||
{tabs.map(({ label }) => (
|
{filteredTabs.map(({ label }) => (
|
||||||
<Tab key={label} whiteSpace="pre">
|
<Tab key={label} whiteSpace="pre">
|
||||||
{label}
|
{label}
|
||||||
</Tab>
|
</Tab>
|
||||||
@ -127,7 +148,7 @@ const UserView = () => {
|
|||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{tabs.map(({ label }) => (
|
{filteredTabs.map(({ label }) => (
|
||||||
<TabPanel key={label} p={0}>
|
<TabPanel key={label} p={0}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<Spinner />}>
|
<Suspense fallback={<Spinner />}>
|
||||||
|
@ -5049,6 +5049,11 @@ json-schema@0.4.0, json-schema@^0.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||||
|
|
||||||
|
json-stringify-deterministic@^1.0.11:
|
||||||
|
version "1.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/json-stringify-deterministic/-/json-stringify-deterministic-1.0.11.tgz#9e53b7431fa5b41d3badedb7bbbe2647ea74f036"
|
||||||
|
integrity sha512-lsn3NoTZ6dGgJJ7W4i7BUKV4WJ+hqAJ0imqHl314MNUw2U+As++qLDudcHqBqlkCXTTH7kH3v5LUQ3CHoVM0BA==
|
||||||
|
|
||||||
json-stringify-safe@~5.0.1:
|
json-stringify-safe@~5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user