mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 19:38:07 +02:00
rebuild timeline loader
This commit is contained in:
parent
14421ea115
commit
b23fe91476
5
.changeset/fresh-dodos-cheat.md
Normal file
5
.changeset/fresh-dodos-cheat.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Rebuild timeline loader class
|
5
.changeset/yellow-crabs-learn.md
Normal file
5
.changeset/yellow-crabs-learn.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Remove broken discover tab
|
@ -10,7 +10,6 @@ import SettingsView from "./views/settings";
|
|||||||
import LoginView from "./views/login";
|
import LoginView from "./views/login";
|
||||||
import ProfileView from "./views/profile";
|
import ProfileView from "./views/profile";
|
||||||
import FollowingTab from "./views/home/following-tab";
|
import FollowingTab from "./views/home/following-tab";
|
||||||
import DiscoverTab from "./views/home/discover-tab";
|
|
||||||
import GlobalTab from "./views/home/global-tab";
|
import GlobalTab from "./views/home/global-tab";
|
||||||
import HashTagView from "./views/hashtag";
|
import HashTagView from "./views/hashtag";
|
||||||
import UserView from "./views/user";
|
import UserView from "./views/user";
|
||||||
@ -102,7 +101,6 @@ const router = createHashRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ path: "", element: <FollowingTab /> },
|
{ path: "", element: <FollowingTab /> },
|
||||||
{ path: "following", element: <FollowingTab /> },
|
{ path: "following", element: <FollowingTab /> },
|
||||||
{ path: "discover", element: <DiscoverTab /> },
|
|
||||||
{ path: "global", element: <GlobalTab /> },
|
{ path: "global", element: <GlobalTab /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -4,84 +4,210 @@ import { NostrEvent } from "../types/nostr-event";
|
|||||||
import { NostrQuery } from "../types/nostr-query";
|
import { NostrQuery } from "../types/nostr-query";
|
||||||
import { NostrRequest } from "./nostr-request";
|
import { NostrRequest } from "./nostr-request";
|
||||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
||||||
import { PersistentSubject } from "./subject";
|
import Subject, { PersistentSubject } from "./subject";
|
||||||
|
|
||||||
type Options = {
|
const BLOCK_SIZE = 10;
|
||||||
name?: string;
|
|
||||||
pageSize: number;
|
|
||||||
startLimit: number;
|
|
||||||
};
|
|
||||||
export type TimelineLoaderOptions = Partial<Options>;
|
|
||||||
|
|
||||||
export class TimelineLoader {
|
type EventFilter = (event: NostrEvent) => boolean;
|
||||||
relays: string[];
|
|
||||||
|
class RelayTimelineLoader {
|
||||||
|
relay: string;
|
||||||
query: NostrQuery;
|
query: NostrQuery;
|
||||||
events = new PersistentSubject<NostrEvent[]>([]);
|
blockSize = BLOCK_SIZE;
|
||||||
loading = new PersistentSubject(false);
|
|
||||||
page = new PersistentSubject(0);
|
loading = false;
|
||||||
|
events: NostrEvent[] = [];
|
||||||
|
/** set to true when the next block produces 0 events */
|
||||||
|
complete = false;
|
||||||
|
|
||||||
|
onEvent = new Subject<NostrEvent>();
|
||||||
|
onBlockFinish = new Subject<void>();
|
||||||
|
|
||||||
|
constructor(relay: string, query: NostrQuery) {
|
||||||
|
this.relay = relay;
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextBlock() {
|
||||||
|
this.loading = true;
|
||||||
|
const query: NostrQuery = { ...this.query, limit: this.blockSize };
|
||||||
|
if (this.events[this.events.length - 1]) {
|
||||||
|
query.until = this.events[this.events.length - 1].created_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = new NostrRequest([this.relay]);
|
||||||
|
|
||||||
|
let gotEvents = 0;
|
||||||
|
request.onEvent.subscribe((e) => {
|
||||||
|
if (this.handleEvent(e)) {
|
||||||
|
gotEvents++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
request.onComplete.then(() => {
|
||||||
|
this.loading = false;
|
||||||
|
if (gotEvents === 0) this.complete = true;
|
||||||
|
this.onBlockFinish.next();
|
||||||
|
});
|
||||||
|
|
||||||
|
request.start(query);
|
||||||
|
}
|
||||||
|
|
||||||
private seenEvents = new Set<string>();
|
private seenEvents = new Set<string>();
|
||||||
|
private handleEvent(event: NostrEvent) {
|
||||||
|
if (!this.seenEvents.has(event.id)) {
|
||||||
|
this.seenEvents.add(event.id);
|
||||||
|
this.events = utils.insertEventIntoDescendingList(Array.from(this.events), event);
|
||||||
|
this.onEvent.next(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastEvent(nth = 0, filter?: EventFilter) {
|
||||||
|
const events = filter ? this.events.filter(filter) : this.events;
|
||||||
|
for (let i = nth; i >= 0; i--) {
|
||||||
|
const event = events[events.length - 1 - i];
|
||||||
|
if (event) return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TimelineLoader {
|
||||||
|
cursor = dayjs().unix();
|
||||||
|
query: NostrQuery;
|
||||||
|
relays: string[];
|
||||||
|
|
||||||
|
events = new PersistentSubject<NostrEvent[]>([]);
|
||||||
|
timeline = new PersistentSubject<NostrEvent[]>([]);
|
||||||
|
loading = new PersistentSubject(false);
|
||||||
|
complete = new PersistentSubject(false);
|
||||||
|
|
||||||
|
loadNextBlockBuffer = 2;
|
||||||
|
eventFilter?: (event: NostrEvent) => boolean;
|
||||||
|
|
||||||
private subscription: NostrMultiSubscription;
|
private subscription: NostrMultiSubscription;
|
||||||
private opts: Options = { pageSize: 60*60, startLimit: 10 };
|
|
||||||
|
|
||||||
constructor(relays: string[], query: NostrQuery, opts?: TimelineLoaderOptions) {
|
private relayTimelineLoaders = new Map<string, RelayTimelineLoader>();
|
||||||
|
|
||||||
|
constructor(relays: string[], query: NostrQuery, name?: string) {
|
||||||
|
this.query = query;
|
||||||
this.relays = relays;
|
this.relays = relays;
|
||||||
Object.assign(this.opts, opts);
|
|
||||||
this.query = { ...query, limit: this.opts.startLimit };
|
|
||||||
|
|
||||||
this.subscription = new NostrMultiSubscription(relays, query, opts?.name);
|
|
||||||
|
|
||||||
|
this.subscription = new NostrMultiSubscription(relays, { ...query, limit: BLOCK_SIZE / 2 }, name);
|
||||||
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
||||||
|
|
||||||
|
this.createLoaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuery(query: NostrQuery) {
|
private seenEvents = new Set<string>();
|
||||||
this.query = { ...query, limit: this.opts.startLimit };
|
|
||||||
this.subscription.setQuery(this.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRelays(relays: string[]) {
|
|
||||||
this.relays = relays;
|
|
||||||
this.subscription.setRelays(relays);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleEvent(event: NostrEvent) {
|
private handleEvent(event: NostrEvent) {
|
||||||
if (!this.seenEvents.has(event.id)) {
|
if (!this.seenEvents.has(event.id)) {
|
||||||
this.seenEvents.add(event.id);
|
this.seenEvents.add(event.id);
|
||||||
this.events.next(utils.insertEventIntoDescendingList(Array.from(this.events.value), event));
|
this.events.next(utils.insertEventIntoDescendingList(Array.from(this.events.value), event));
|
||||||
if (this.loading.value) this.loading.next(false);
|
|
||||||
|
if (!this.eventFilter || this.eventFilter(event)) {
|
||||||
|
this.timeline.next(utils.insertEventIntoDescendingList(Array.from(this.timeline.value), event));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPageDates(page: number) {
|
private createLoaders() {
|
||||||
const start = this.events.value[0]?.created_at ?? dayjs().unix();
|
for (const relay of this.relays) {
|
||||||
const until = start - page * this.opts.pageSize;
|
if (!this.relayTimelineLoaders.has(relay)) {
|
||||||
const since = until - this.opts.pageSize;
|
const loader = new RelayTimelineLoader(relay, this.query);
|
||||||
|
this.relayTimelineLoaders.set(relay, loader);
|
||||||
return {
|
loader.onEvent.subscribe(this.handleEvent, this);
|
||||||
until,
|
loader.onBlockFinish.subscribe(this.updateLoading, this);
|
||||||
since,
|
loader.onBlockFinish.subscribe(this.updateComplete, this);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private removeLoaders(filter?: (loader: RelayTimelineLoader) => boolean) {
|
||||||
|
for (const [relay, loader] of this.relayTimelineLoaders) {
|
||||||
|
if (!filter || filter(loader)) {
|
||||||
|
loader?.onEvent.unsubscribe(this.handleEvent, this);
|
||||||
|
loader?.onBlockFinish.unsubscribe(this.updateLoading, this);
|
||||||
|
loader?.onBlockFinish.unsubscribe(this.updateComplete, this);
|
||||||
|
this.relayTimelineLoaders.delete(relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMore() {
|
setRelays(relays: string[]) {
|
||||||
if (this.loading.value) return;
|
// remove loaders
|
||||||
|
this.removeLoaders((loader) => !relays.includes(loader.relay));
|
||||||
|
|
||||||
const query = { ...this.query, ...this.getPageDates(this.page.value) };
|
this.relays = relays;
|
||||||
const request = new NostrRequest(this.relays);
|
this.createLoaders();
|
||||||
request.onEvent.subscribe(this.handleEvent, this);
|
|
||||||
request.onComplete.then(() => {
|
|
||||||
this.loading.next(false);
|
|
||||||
});
|
|
||||||
request.start(query);
|
|
||||||
|
|
||||||
this.loading.next(true);
|
this.subscription.setRelays(relays);
|
||||||
this.page.next(this.page.value + 1);
|
|
||||||
}
|
}
|
||||||
|
setQuery(query: NostrQuery) {
|
||||||
|
this.removeLoaders();
|
||||||
|
|
||||||
forgetEvents() {
|
this.query = query;
|
||||||
this.events.next([]);
|
this.events.next([]);
|
||||||
|
this.timeline.next([]);
|
||||||
this.seenEvents.clear();
|
this.seenEvents.clear();
|
||||||
|
|
||||||
|
this.createLoaders();
|
||||||
|
|
||||||
|
// update the subscription
|
||||||
this.subscription.forgetEvents();
|
this.subscription.forgetEvents();
|
||||||
|
this.subscription.setQuery({ ...query, limit: BLOCK_SIZE / 2 });
|
||||||
|
}
|
||||||
|
setFilter(filter?: (event: NostrEvent) => boolean) {
|
||||||
|
this.eventFilter = filter;
|
||||||
|
if (this.eventFilter) {
|
||||||
|
this.timeline.next(this.events.value.filter(this.eventFilter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCursor(cursor: number) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
this.loadNextBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextBlocks() {
|
||||||
|
let triggeredLoad = false;
|
||||||
|
for (const [relay, loader] of this.relayTimelineLoaders) {
|
||||||
|
if (loader.complete || loader.loading) continue;
|
||||||
|
const event = loader.getLastEvent(this.loadNextBlockBuffer, this.eventFilter);
|
||||||
|
if (!event || event.created_at >= this.cursor) {
|
||||||
|
loader.loadNextBlock();
|
||||||
|
triggeredLoad = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (triggeredLoad) this.updateLoading();
|
||||||
|
}
|
||||||
|
/** @deprecated */
|
||||||
|
loadMore() {
|
||||||
|
for (const [relay, loader] of this.relayTimelineLoaders) {
|
||||||
|
if (loader.complete || loader.loading) continue;
|
||||||
|
loader.loadNextBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLoading() {
|
||||||
|
for (const [relay, loader] of this.relayTimelineLoaders) {
|
||||||
|
if (loader.loading) {
|
||||||
|
if (!this.loading.value) {
|
||||||
|
this.loading.next(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.loading.value) this.loading.next(false);
|
||||||
|
}
|
||||||
|
private updateComplete() {
|
||||||
|
if (this.complete.value) return;
|
||||||
|
for (const [relay, loader] of this.relayTimelineLoaders) {
|
||||||
|
if (!loader.complete) {
|
||||||
|
this.complete.next(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.complete.next(true);
|
||||||
}
|
}
|
||||||
open() {
|
open() {
|
||||||
this.subscription.open();
|
this.subscription.open();
|
||||||
@ -89,4 +215,13 @@ export class TimelineLoader {
|
|||||||
close() {
|
close() {
|
||||||
this.subscription.close();
|
this.subscription.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this is only needed because the current logic dose not remove events when the relay they where fetched from is removed
|
||||||
|
/** @deprecated */
|
||||||
|
forgetEvents() {
|
||||||
|
this.events.next([]);
|
||||||
|
this.timeline.next([]);
|
||||||
|
this.seenEvents.clear();
|
||||||
|
this.subscription.forgetEvents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,80 +1,8 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
|
||||||
import { NostrQuery } from "../types/nostr-query";
|
|
||||||
import { NostrRequest } from "./nostr-request";
|
|
||||||
import { NostrMultiSubscription } from "./nostr-multi-subscription";
|
|
||||||
import { PersistentSubject } from "./subject";
|
|
||||||
import { utils } from "nostr-tools";
|
|
||||||
import { truncatedId } from "../helpers/nostr-event";
|
import { truncatedId } from "../helpers/nostr-event";
|
||||||
|
import { TimelineLoader } from "./timeline-loader";
|
||||||
|
|
||||||
const PAGE_SIZE = 60 * 60 * 24 * 7; //in seconds
|
export default class UserTimeline extends TimelineLoader {
|
||||||
|
|
||||||
export default class UserTimeline {
|
|
||||||
pubkey: string;
|
|
||||||
query: NostrQuery;
|
|
||||||
|
|
||||||
events = new PersistentSubject<NostrEvent[]>([]);
|
|
||||||
loading = new PersistentSubject(false);
|
|
||||||
page = new PersistentSubject(0);
|
|
||||||
|
|
||||||
private seenEvents = new Set<string>();
|
|
||||||
private subscription: NostrMultiSubscription;
|
|
||||||
|
|
||||||
constructor(pubkey: string) {
|
constructor(pubkey: string) {
|
||||||
this.pubkey = pubkey;
|
super([], { authors: [pubkey], kinds: [1, 6] }, truncatedId(pubkey) + "-timeline");
|
||||||
this.query = { authors: [pubkey], kinds: [1, 6], limit: 20 };
|
|
||||||
|
|
||||||
this.subscription = new NostrMultiSubscription([], this.query, truncatedId(pubkey) + "-timeline");
|
|
||||||
this.subscription.onEvent.subscribe(this.handleEvent, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRelays(relays: string[]) {
|
|
||||||
this.subscription.setRelays(relays);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleEvent(event: NostrEvent) {
|
|
||||||
if (!this.seenEvents.has(event.id)) {
|
|
||||||
this.seenEvents.add(event.id);
|
|
||||||
this.events.next(utils.insertEventIntoDescendingList(Array.from(this.events.value), event));
|
|
||||||
if (this.loading.value) this.loading.next(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPageDates(page: number) {
|
|
||||||
const start = this.events.value[0]?.created_at ?? dayjs().unix();
|
|
||||||
const until = start - page * PAGE_SIZE;
|
|
||||||
const since = until - PAGE_SIZE;
|
|
||||||
|
|
||||||
return {
|
|
||||||
until,
|
|
||||||
since,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMore() {
|
|
||||||
if (this.loading.value) return;
|
|
||||||
|
|
||||||
const query = { ...this.query, ...this.getPageDates(this.page.value) };
|
|
||||||
const request = new NostrRequest(this.subscription.relayUrls);
|
|
||||||
request.onEvent.subscribe(this.handleEvent, this);
|
|
||||||
request.onComplete.then(() => {
|
|
||||||
this.loading.next(false);
|
|
||||||
});
|
|
||||||
request.start(query);
|
|
||||||
|
|
||||||
this.loading.next(true);
|
|
||||||
this.page.next(this.page.value + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
forgetEvents() {
|
|
||||||
this.events.next([]);
|
|
||||||
this.seenEvents.clear();
|
|
||||||
this.subscription.forgetEvents();
|
|
||||||
}
|
|
||||||
open() {
|
|
||||||
this.subscription.open();
|
|
||||||
}
|
|
||||||
close() {
|
|
||||||
this.subscription.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
|||||||
|
|
||||||
export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNostrEvent) {
|
export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNostrEvent) {
|
||||||
return embedJSX(content, {
|
return embedJSX(content, {
|
||||||
regexp: /:([a-zA-Z0-9]+):/i,
|
regexp: /:([a-zA-Z0-9_]+):/i,
|
||||||
render: (match) => {
|
render: (match) => {
|
||||||
const emojiTag = note.tags.find(
|
const emojiTag = note.tags.find(
|
||||||
(tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2]
|
(tag) => tag[0] === "emoji" && tag[1].toLowerCase() === match[1].toLowerCase() && tag[2]
|
||||||
|
27
src/components/load-more-button.tsx
Normal file
27
src/components/load-more-button.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Alert, AlertIcon, Button, Spinner } from "@chakra-ui/react";
|
||||||
|
import { TimelineLoader } from "../classes/timeline-loader";
|
||||||
|
import useSubject from "../hooks/use-subject";
|
||||||
|
|
||||||
|
export default function LoadMoreButton({ timeline }: { timeline: TimelineLoader }) {
|
||||||
|
const loading = useSubject(timeline.loading);
|
||||||
|
const complete = useSubject(timeline.complete);
|
||||||
|
|
||||||
|
if (complete) {
|
||||||
|
return (
|
||||||
|
<Alert status="info" flexShrink={0}>
|
||||||
|
<AlertIcon />
|
||||||
|
No more events
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={() => timeline.loadMore()} flexShrink={0} size="lg" mx="auto" minW="lg">
|
||||||
|
Load More
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
|
import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import { parseZapNote, totalZaps } from "../../helpers/zaps";
|
import { parseZapEvent, totalZaps } from "../../helpers/zaps";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import useEventZaps from "../../hooks/use-event-zaps";
|
import useEventZaps from "../../hooks/use-event-zaps";
|
||||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
@ -19,7 +19,7 @@ export default function NoteZapButton({ note, ...props }: { note: NostrEvent } &
|
|||||||
const parsed = [];
|
const parsed = [];
|
||||||
for (const zap of zaps) {
|
for (const zap of zaps) {
|
||||||
try {
|
try {
|
||||||
parsed.push(parseZapNote(zap));
|
parsed.push(parseZapEvent(zap));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
|
@ -17,7 +17,7 @@ import { UserAvatarLink } from "../user-avatar-link";
|
|||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
|
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
|
||||||
import { parseZapNote } from "../../helpers/zaps";
|
import { parseZapEvent } from "../../helpers/zaps";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import useEventReactions from "../../hooks/use-event-reactions";
|
import useEventReactions from "../../hooks/use-event-reactions";
|
||||||
import useEventZaps from "../../hooks/use-event-zaps";
|
import useEventZaps from "../../hooks/use-event-zaps";
|
||||||
@ -50,7 +50,7 @@ const ReactionEvent = React.memo(({ event }: { event: NostrEvent }) => (
|
|||||||
const ZapEvent = React.memo(({ event }: { event: NostrEvent }) => {
|
const ZapEvent = React.memo(({ event }: { event: NostrEvent }) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
try {
|
try {
|
||||||
const { payment, request } = parseZapNote(event);
|
const { payment, request } = parseZapEvent(event);
|
||||||
|
|
||||||
if (!payment.amount) return null;
|
if (!payment.amount) return null;
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ export function totalZaps(events: NostrEvent[]) {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseZapNote(event: NostrEvent) {
|
function parseZapEvent(event: NostrEvent) {
|
||||||
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
||||||
if (!zapRequestStr) throw new Error("no description tag");
|
if (!zapRequestStr) throw new Error("no description tag");
|
||||||
|
|
||||||
@ -77,3 +77,14 @@ export function parseZapNote(event: NostrEvent) {
|
|||||||
eventId,
|
eventId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zapEventCache = new Map<string, ReturnType<typeof parseZapEvent>>();
|
||||||
|
function cachedParseZapEvent(event: NostrEvent) {
|
||||||
|
let result = zapEventCache.get(event.id);
|
||||||
|
if (result) return result;
|
||||||
|
result = parseZapEvent(event);
|
||||||
|
if (result) zapEventCache.set(event.id, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { cachedParseZapEvent as parseZapEvent };
|
||||||
|
18
src/hooks/use-scroll-position.ts
Normal file
18
src/hooks/use-scroll-position.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { MutableRefObject, useState } from "react";
|
||||||
|
import { useInterval } from "react-use";
|
||||||
|
|
||||||
|
export default function useScrollPosition(ref: MutableRefObject<HTMLDivElement | null>, interval = 1000) {
|
||||||
|
const [percent, setPercent] = useState(0);
|
||||||
|
useInterval(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const scrollBottom = ref.current.scrollTop + ref.current.getClientRects()[0].height;
|
||||||
|
|
||||||
|
if (ref.current.scrollHeight === 0) {
|
||||||
|
return setPercent(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollPosition = Math.min(scrollBottom / ref.current.scrollHeight, 1);
|
||||||
|
setPercent(scrollPosition);
|
||||||
|
}, interval);
|
||||||
|
return percent;
|
||||||
|
}
|
@ -1,27 +1,37 @@
|
|||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useUnmount } from "react-use";
|
import { useUnmount } from "react-use";
|
||||||
import { TimelineLoader, TimelineLoaderOptions } from "../classes/timeline-loader";
|
import { TimelineLoader } from "../classes/timeline-loader";
|
||||||
import { NostrQuery } from "../types/nostr-query";
|
import { NostrQuery } from "../types/nostr-query";
|
||||||
import useSubject from "./use-subject";
|
import useSubject from "./use-subject";
|
||||||
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
|
||||||
type Options = TimelineLoaderOptions & {
|
type Options = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
eventFilter?: (event: NostrEvent) => boolean;
|
||||||
|
cursor?: number;
|
||||||
|
name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useTimelineLoader(key: string, relays: string[], query: NostrQuery, opts?: Options) {
|
export function useTimelineLoader(key: string, relays: string[], query: NostrQuery, opts?: Options) {
|
||||||
if (opts && !opts.name) opts.name = key;
|
if (opts && !opts.name) opts.name = key;
|
||||||
|
|
||||||
const ref = useRef<TimelineLoader | null>(null);
|
const ref = useRef<TimelineLoader | null>(null);
|
||||||
const loader = (ref.current = ref.current || new TimelineLoader(relays, query, opts));
|
const loader = (ref.current = ref.current || new TimelineLoader(relays, query, opts?.name));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loader.forgetEvents();
|
|
||||||
loader.setQuery(query);
|
loader.setQuery(query);
|
||||||
}, [key]);
|
}, [JSON.stringify(query)]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loader.setRelays(relays);
|
loader.setRelays(relays);
|
||||||
}, [relays.join("|")]);
|
}, [relays.join("|")]);
|
||||||
|
useEffect(() => {
|
||||||
|
loader.setFilter(opts?.eventFilter);
|
||||||
|
}, [opts?.eventFilter]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (opts?.cursor !== undefined) {
|
||||||
|
loader.setCursor(opts.cursor);
|
||||||
|
}
|
||||||
|
}, [opts?.cursor]);
|
||||||
|
|
||||||
const enabled = opts?.enabled ?? true;
|
const enabled = opts?.enabled ?? true;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -35,17 +45,14 @@ export function useTimelineLoader(key: string, relays: string[], query: NostrQue
|
|||||||
loader.close();
|
loader.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
const events = useSubject(loader.events);
|
const timeline = useSubject(loader.timeline);
|
||||||
const loading = useSubject(loader.loading);
|
const loading = useSubject(loader.loading);
|
||||||
|
const complete = useSubject(loader.complete);
|
||||||
const loadMore = useCallback(() => {
|
|
||||||
if (enabled) loader.loadMore();
|
|
||||||
}, [enabled]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loader,
|
loader,
|
||||||
events,
|
timeline,
|
||||||
loading,
|
loading,
|
||||||
loadMore,
|
complete,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -215,7 +215,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
|
|||||||
fields
|
fields
|
||||||
.filter((item) => item.multiple && item.fieldName && item.fieldName.match("(ogImage|ogVideo|twitter|musicSong).*"))
|
.filter((item) => item.multiple && item.fieldName && item.fieldName.match("(ogImage|ogVideo|twitter|musicSong).*"))
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
delete ogObject[item.fieldName];
|
delete ogObject[item.fieldName];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Spinner,
|
|
||||||
Switch,
|
Switch,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
useEditableControls,
|
useEditableControls,
|
||||||
@ -22,8 +21,10 @@ import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
|||||||
import { isReply } from "../../helpers/nostr-event";
|
import { isReply } from "../../helpers/nostr-event";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { CheckIcon, EditIcon, RelayIcon } from "../../components/icons";
|
import { CheckIcon, EditIcon, RelayIcon } from "../../components/icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import RelaySelectionModal from "./relay-selection-modal";
|
import RelaySelectionModal from "./relay-selection-modal";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
function EditableControls() {
|
function EditableControls() {
|
||||||
const { isEditing, getSubmitButtonProps, getCancelButtonProps, getEditButtonProps } = useEditableControls();
|
const { isEditing, getSubmitButtonProps, getCancelButtonProps, getEditButtonProps } = useEditableControls();
|
||||||
@ -51,18 +52,23 @@ export default function HashTagView() {
|
|||||||
|
|
||||||
const relaysModal = useDisclosure();
|
const relaysModal = useDisclosure();
|
||||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||||
const { events, loading, loadMore, loader } = useTimelineLoader(
|
|
||||||
|
const eventFilter = useCallback(
|
||||||
|
(event: NostrEvent) => {
|
||||||
|
return showReplies ? true : !isReply(event);
|
||||||
|
},
|
||||||
|
[showReplies]
|
||||||
|
);
|
||||||
|
const { timeline, loader } = useTimelineLoader(
|
||||||
`${hashtag}-hashtag`,
|
`${hashtag}-hashtag`,
|
||||||
selectedRelays,
|
selectedRelays,
|
||||||
{ kinds: [1], "#t": [hashtag] },
|
{ kinds: [1], "#t": [hashtag] },
|
||||||
{ pageSize: 60 * 10 }
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex direction="column" gap="4" overflow="auto" flex={1} pb="4" pt="4" pl="1" pr="1">
|
<Flex direction="column" gap="4" overflowY="auto" overflowX="hidden" flex={1} pb="4" pt="4" pl="1" pr="1">
|
||||||
<Flex gap="4" alignItems="center" wrap="wrap">
|
<Flex gap="4" alignItems="center" wrap="wrap">
|
||||||
<Editable
|
<Editable
|
||||||
value={editableHashtag}
|
value={editableHashtag}
|
||||||
@ -95,13 +101,8 @@ export default function HashTagView() {
|
|||||||
{timeline.map((event) => (
|
{timeline.map((event) => (
|
||||||
<Note key={event.id} event={event} maxHeight={600} />
|
<Note key={event.id} event={event} maxHeight={600} />
|
||||||
))}
|
))}
|
||||||
{loading ? (
|
|
||||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
<LoadMoreButton timeline={loader} />
|
||||||
) : (
|
|
||||||
<Button onClick={() => loadMore()} flexShrink={0}>
|
|
||||||
Load More
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{relaysModal.isOpen && (
|
{relaysModal.isOpen && (
|
||||||
|
@ -1,97 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { Button, Flex, Spinner } from "@chakra-ui/react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { Note } from "../../components/note";
|
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
|
||||||
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
|
||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
|
||||||
import userContactsService, { UserContacts } from "../../services/user-contacts";
|
|
||||||
import { PersistentSubject } from "../../classes/subject";
|
|
||||||
import useSubject from "../../hooks/use-subject";
|
|
||||||
import { useThrottle } from "react-use";
|
|
||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
|
||||||
|
|
||||||
class DiscoverContacts {
|
|
||||||
pubkey: string;
|
|
||||||
relays: string[];
|
|
||||||
pubkeys = new PersistentSubject<string[]>([]);
|
|
||||||
|
|
||||||
constructor(pubkey: string, relays: string[]) {
|
|
||||||
this.pubkey = pubkey;
|
|
||||||
this.relays = relays;
|
|
||||||
|
|
||||||
userContactsService.requestContacts(pubkey, relays).subscribe(this.handleContacts, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private personalContacts: UserContacts | undefined;
|
|
||||||
handleContacts(contacts: UserContacts) {
|
|
||||||
if (contacts.pubkey === this.pubkey) {
|
|
||||||
this.personalContacts = contacts;
|
|
||||||
|
|
||||||
// unsubscribe from old contacts
|
|
||||||
if (this.pubkeys.value.length > 0) {
|
|
||||||
for (const key of this.pubkeys.value) {
|
|
||||||
userContactsService.getSubject(key).unsubscribe(this.handleContacts, this);
|
|
||||||
}
|
|
||||||
this.pubkeys.next([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// request new contacts
|
|
||||||
for (const key of contacts.contacts) {
|
|
||||||
userContactsService.requestContacts(key, this.relays).subscribe(this.handleContacts, this);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// add the pubkeys to contacts
|
|
||||||
const keysToAdd = contacts.contacts.filter(
|
|
||||||
(key) =>
|
|
||||||
(!this.personalContacts || !this.personalContacts.contacts.includes(key)) && !this.pubkeys.value.includes(key)
|
|
||||||
);
|
|
||||||
this.pubkeys.next([...this.pubkeys.value, ...keysToAdd]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
userContactsService.getSubject(this.pubkey).unsubscribe(this.handleContacts, this);
|
|
||||||
for (const key of this.pubkeys.value) {
|
|
||||||
userContactsService.getSubject(key).unsubscribe(this.handleContacts, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiscoverTabBody() {
|
|
||||||
useAppTitle("discover");
|
|
||||||
const account = useCurrentAccount()!;
|
|
||||||
const relays = useReadRelayUrls();
|
|
||||||
|
|
||||||
const discover = useMemo(() => new DiscoverContacts(account.pubkey, relays), [account.pubkey, relays.join("|")]);
|
|
||||||
const pubkeys = useSubject(discover.pubkeys);
|
|
||||||
const throttledPubkeys = useThrottle(pubkeys, 1000);
|
|
||||||
|
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
|
||||||
`${truncatedId(account.pubkey)}-discover`,
|
|
||||||
relays,
|
|
||||||
{ authors: throttledPubkeys, kinds: [1], since: dayjs().subtract(1, "hour").unix() },
|
|
||||||
{ pageSize: 60 * 60, enabled: throttledPubkeys.length > 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeline = events.filter((e) => !isReply(e));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" gap="2">
|
|
||||||
{timeline.map((event) => (
|
|
||||||
<Note key={event.id} event={event} maxHeight={600} />
|
|
||||||
))}
|
|
||||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DiscoverTab() {
|
|
||||||
return (
|
|
||||||
<RequireCurrentAccount>
|
|
||||||
<DiscoverTabBody />
|
|
||||||
</RequireCurrentAccount>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,17 +1,20 @@
|
|||||||
import { Button, Flex, FormControl, FormLabel, Spinner, Switch } from "@chakra-ui/react";
|
import { Button, Flex, FormControl, FormLabel, Spinner, Switch } from "@chakra-ui/react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import { useInterval } from "react-use";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||||
import { AddIcon } from "@chakra-ui/icons";
|
import { AddIcon } from "@chakra-ui/icons";
|
||||||
import { useContext } from "react";
|
import { useCallback, useContext, useRef } from "react";
|
||||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import RepostNote from "../../components/repost-note";
|
import RepostNote from "../../components/repost-note";
|
||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import useScrollPosition from "../../hooks/use-scroll-position";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
function FollowingTabBody() {
|
function FollowingTabBody() {
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
@ -24,19 +27,38 @@ function FollowingTabBody() {
|
|||||||
showReplies ? setSearch({}) : setSearch({ replies: "show" });
|
showReplies ? setSearch({}) : setSearch({ replies: "show" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const following = contacts?.contacts || [];
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const scrollPosition = useScrollPosition(scrollBox);
|
||||||
`${truncatedId(account.pubkey)}-following-posts`,
|
|
||||||
readRelays,
|
const eventFilter = useCallback(
|
||||||
{ authors: following, kinds: [1, 6], since: dayjs().subtract(2, "hour").unix() },
|
(event: NostrEvent) => {
|
||||||
{ pageSize: 60 * 60, enabled: following.length > 0 }
|
if (!showReplies && isReply(event)) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[showReplies]
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
const following = contacts?.contacts || [];
|
||||||
|
const { timeline, loader } = useTimelineLoader(
|
||||||
|
`${truncatedId(account.pubkey)}-following`,
|
||||||
|
readRelays,
|
||||||
|
{ authors: following, kinds: [1, 6] },
|
||||||
|
{ enabled: following.length > 0, eventFilter }
|
||||||
|
);
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
if (scrollPosition > 0.9) loader.loadMore();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2">
|
<Flex py="4" direction="column" gap="2" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
||||||
<Button variant="outline" leftIcon={<AddIcon />} onClick={() => openModal()} isDisabled={account.readonly}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
leftIcon={<AddIcon />}
|
||||||
|
onClick={() => openModal()}
|
||||||
|
isDisabled={account.readonly}
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
New Post
|
New Post
|
||||||
</Button>
|
</Button>
|
||||||
<FormControl display="flex" alignItems="center">
|
<FormControl display="flex" alignItems="center">
|
||||||
@ -52,7 +74,8 @@ function FollowingTabBody() {
|
|||||||
<Note key={event.id} event={event} maxHeight={600} />
|
<Note key={event.id} event={event} maxHeight={600} />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
|
||||||
|
<LoadMoreButton timeline={loader} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Button, Flex, FormControl, FormLabel, Select, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
import { useCallback } from "react";
|
||||||
|
import { Flex, FormControl, FormLabel, Select, Switch, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { unique } from "../../helpers/array";
|
import { unique } from "../../helpers/array";
|
||||||
@ -6,6 +7,8 @@ import { isReply } from "../../helpers/nostr-event";
|
|||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
export default function GlobalTab() {
|
export default function GlobalTab() {
|
||||||
useAppTitle("global");
|
useAppTitle("global");
|
||||||
@ -17,21 +20,27 @@ export default function GlobalTab() {
|
|||||||
setSearchParams({ relay: url });
|
setSearchParams({ relay: url });
|
||||||
} else setSearchParams({});
|
} else setSearchParams({});
|
||||||
};
|
};
|
||||||
|
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||||
|
|
||||||
const availableRelays = unique([...defaultRelays, selectedRelay]).filter(Boolean);
|
const availableRelays = unique([...defaultRelays, selectedRelay]).filter(Boolean);
|
||||||
|
|
||||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
const eventFilter = useCallback(
|
||||||
const { events, loading, loadMore, loader } = useTimelineLoader(
|
(event: NostrEvent) => {
|
||||||
|
if (!showReplies && isReply(event)) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[showReplies]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { timeline, loader } = useTimelineLoader(
|
||||||
`global`,
|
`global`,
|
||||||
selectedRelay ? [selectedRelay] : [],
|
selectedRelay ? [selectedRelay] : [],
|
||||||
{ kinds: [1] },
|
{ kinds: [1] },
|
||||||
{ pageSize: 60*10 }
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2">
|
<Flex py="4" direction="column" gap="2" overflowY="auto" overflowX="hidden">
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Select
|
<Select
|
||||||
placeholder="Select Relay"
|
placeholder="Select Relay"
|
||||||
@ -39,7 +48,6 @@ export default function GlobalTab() {
|
|||||||
value={selectedRelay}
|
value={selectedRelay}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedRelay(e.target.value);
|
setSelectedRelay(e.target.value);
|
||||||
loader.forgetEvents();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{availableRelays.map((url) => (
|
{availableRelays.map((url) => (
|
||||||
@ -58,7 +66,8 @@ export default function GlobalTab() {
|
|||||||
{timeline.map((event) => (
|
{timeline.map((event) => (
|
||||||
<Note key={event.id} event={event} maxHeight={600} />
|
<Note key={event.id} event={event} maxHeight={600} />
|
||||||
))}
|
))}
|
||||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
|
||||||
|
<LoadMoreButton timeline={loader} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { Outlet, useMatches, useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: "Following", path: "/following" },
|
{ label: "Following", path: "/following" },
|
||||||
{ label: "Discover", path: "/discover" },
|
// { label: "Discover", path: "/discover" },
|
||||||
// { label: "Popular", path: "/popular" },
|
// { label: "Popular", path: "/popular" },
|
||||||
{ label: "Global", path: "/global" },
|
{ label: "Global", path: "/global" },
|
||||||
];
|
];
|
||||||
@ -30,9 +30,9 @@ export default function HomeView() {
|
|||||||
<Tab key={label}>{label}</Tab>
|
<Tab key={label}>{label}</Tab>
|
||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels overflow="auto" height="100%">
|
<TabPanels overflow="hidden" h="full">
|
||||||
{tabs.map(({ label }) => (
|
{tabs.map(({ label }) => (
|
||||||
<TabPanel key={label} pr={0} pl={0}>
|
<TabPanel key={label} p={0} overflow="hidden" h="full" display="flex" flexDirection="column">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Button, Card, CardBody, CardHeader, Flex, Spinner, Text } from "@chakra-ui/react";
|
import { memo, useCallback } from "react";
|
||||||
|
import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { memo } from "react";
|
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
@ -9,6 +9,7 @@ import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
|||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { NoteLink } from "../../components/note-link";
|
import { NoteLink } from "../../components/note-link";
|
||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||||
<Card size="sm" variant="outline">
|
<Card size="sm" variant="outline">
|
||||||
@ -37,26 +38,25 @@ const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
|||||||
function NotificationsPage() {
|
function NotificationsPage() {
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
|
||||||
|
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
|
||||||
|
const { timeline, loader } = useTimelineLoader(
|
||||||
"notifications",
|
"notifications",
|
||||||
readRelays,
|
readRelays,
|
||||||
{
|
{
|
||||||
"#p": [account.pubkey],
|
"#p": [account.pubkey],
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
},
|
},
|
||||||
{ pageSize: 60 * 60 * 24 }
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline = events
|
|
||||||
// ignore events made my the user
|
|
||||||
.filter((e) => e.pubkey !== account.pubkey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" overflowX="hidden" overflowY="auto" gap="2">
|
<Flex direction="column" overflowX="hidden" overflowY="auto" gap="2">
|
||||||
{timeline.map((event) => (
|
{timeline.map((event) => (
|
||||||
<NotificationItem key={event.id} event={event} />
|
<NotificationItem key={event.id} event={event} />
|
||||||
))}
|
))}
|
||||||
{loading ? <Spinner ml="auto" mr="auto" mt="8" mb="8" /> : <Button onClick={() => loadMore()}>Load More</Button>}
|
|
||||||
|
<LoadMoreButton timeline={loader} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -155,7 +155,7 @@ export default function UserAboutTab() {
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Accordion allowToggle allowMultiple>
|
<Accordion allowMultiple>
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<h2>
|
<h2>
|
||||||
<AccordionButton>
|
<AccordionButton>
|
||||||
@ -170,9 +170,7 @@ export default function UserAboutTab() {
|
|||||||
<Stat>
|
<Stat>
|
||||||
<StatLabel>Following</StatLabel>
|
<StatLabel>Following</StatLabel>
|
||||||
<StatNumber>{contacts ? readablizeSats(contacts.contacts.length) : "Unknown"}</StatNumber>
|
<StatNumber>{contacts ? readablizeSats(contacts.contacts.length) : "Unknown"}</StatNumber>
|
||||||
{contacts && (
|
{contacts && <StatHelpText>Updated {dayjs.unix(contacts.created_at).fromNow()}</StatHelpText>}
|
||||||
<StatHelpText>Updated {dayjs.unix(contacts.created_at).fromNow()}</StatHelpText>
|
|
||||||
)}
|
|
||||||
</Stat>
|
</Stat>
|
||||||
|
|
||||||
{stats && (
|
{stats && (
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { Box, Button, Flex, Grid, IconButton, Spinner } from "@chakra-ui/react";
|
import { Box, Flex, Grid, IconButton } from "@chakra-ui/react";
|
||||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||||
import { useMount, useUnmount } from "react-use";
|
import { useMount, useUnmount } from "react-use";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
@ -10,6 +10,8 @@ import { ExternalLinkIcon } from "../../components/icons";
|
|||||||
import { getSharableNoteId } from "../../helpers/nip19";
|
import { getSharableNoteId } from "../../helpers/nip19";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import userTimelineService from "../../services/user-timeline";
|
import userTimelineService from "../../services/user-timeline";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
const matchAllImages = new RegExp(matchImageUrls, "ig");
|
const matchAllImages = new RegExp(matchImageUrls, "ig");
|
||||||
|
|
||||||
@ -21,10 +23,13 @@ const UserMediaTab = () => {
|
|||||||
|
|
||||||
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
||||||
|
|
||||||
const events = useSubject(timeline.events);
|
const eventFilter = useCallback((e: NostrEvent) => e.kind === 1 && !!e.content.match(matchAllImages), []);
|
||||||
const loading = useSubject(timeline.loading);
|
useEffect(() => {
|
||||||
|
timeline.setFilter(eventFilter);
|
||||||
|
}, [timeline, eventFilter]);
|
||||||
|
|
||||||
const filteredEvents = useMemo(() => events.filter((e) => e.kind === 1), [events]);
|
const events = useSubject(timeline.timeline);
|
||||||
|
const loading = useSubject(timeline.loading);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
timeline.setRelays(contextRelays);
|
timeline.setRelays(contextRelays);
|
||||||
@ -36,7 +41,7 @@ const UserMediaTab = () => {
|
|||||||
const images = useMemo(() => {
|
const images = useMemo(() => {
|
||||||
var images: { eventId: string; src: string; index: number }[] = [];
|
var images: { eventId: string; src: string; index: number }[] = [];
|
||||||
|
|
||||||
for (const event of filteredEvents) {
|
for (const event of events) {
|
||||||
const urls = event.content.matchAll(matchAllImages);
|
const urls = event.content.matchAll(matchAllImages);
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@ -46,7 +51,7 @@ const UserMediaTab = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return images;
|
return images;
|
||||||
}, [filteredEvents]);
|
}, [events]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" px="2" pb="8" h="full" overflowY="auto">
|
<Flex direction="column" gap="2" px="2" pb="8" h="full" overflowY="auto">
|
||||||
@ -77,13 +82,8 @@ const UserMediaTab = () => {
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</ImageGalleryProvider>
|
</ImageGalleryProvider>
|
||||||
{loading ? (
|
|
||||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
<LoadMoreButton timeline={timeline} />
|
||||||
) : (
|
|
||||||
<Button onClick={() => timeline.loadMore()} flexShrink={0}>
|
|
||||||
Load More
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import { Button, Flex, FormControl, FormLabel, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import RepostNote from "../../components/repost-note";
|
import RepostNote from "../../components/repost-note";
|
||||||
import { isReply, isRepost } from "../../helpers/nostr-event";
|
import { isReply, isRepost } from "../../helpers/nostr-event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
import userTimelineService from "../../services/user-timeline";
|
import userTimelineService from "../../services/user-timeline";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { useMount, useUnmount } from "react-use";
|
import { useInterval, useMount, useUnmount } from "react-use";
|
||||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import useScrollPosition from "../../hooks/use-scroll-position";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
const UserNotesTab = () => {
|
const UserNotesTab = () => {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
@ -17,11 +20,21 @@ const UserNotesTab = () => {
|
|||||||
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
|
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
|
||||||
const { isOpen: hideReposts, onToggle: toggleReposts } = useDisclosure();
|
const { isOpen: hideReposts, onToggle: toggleReposts } = useDisclosure();
|
||||||
|
|
||||||
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
|
const scrollPosition = useScrollPosition(scrollBox);
|
||||||
|
|
||||||
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
||||||
|
const eventFilter = useCallback(
|
||||||
const events = useSubject(timeline.events);
|
(event: NostrEvent) => {
|
||||||
const loading = useSubject(timeline.loading);
|
if (!showReplies && isReply(event)) return false;
|
||||||
|
if (hideReposts && isRepost(event)) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[showReplies, hideReposts]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
timeline.setFilter(eventFilter);
|
||||||
|
}, [timeline, eventFilter]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
timeline.setRelays(readRelays);
|
timeline.setRelays(readRelays);
|
||||||
}, [timeline, readRelays.join("|")]);
|
}, [timeline, readRelays.join("|")]);
|
||||||
@ -29,14 +42,20 @@ const UserNotesTab = () => {
|
|||||||
useMount(() => timeline.open());
|
useMount(() => timeline.open());
|
||||||
useUnmount(() => timeline.close());
|
useUnmount(() => timeline.close());
|
||||||
|
|
||||||
const filteredEvents = events.filter((event) => {
|
useInterval(() => {
|
||||||
if (!showReplies && isReply(event)) return false;
|
const events = timeline.timeline.value;
|
||||||
if (hideReposts && isRepost(event)) return false;
|
if (events.length > 0) {
|
||||||
return true;
|
const eventAtScrollPos = events[Math.floor(scrollPosition * (events.length - 1))];
|
||||||
});
|
timeline.setCursor(eventAtScrollPos.created_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeline.loadNextBlocks();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const eventsTimeline = useSubject(timeline.timeline);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" pt="4" pb="8" h="full" overflowY="auto" overflowX="hidden">
|
<Flex direction="column" gap="2" pt="4" pb="8" h="full" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
||||||
<FormControl display="flex" alignItems="center" mx="2">
|
<FormControl display="flex" alignItems="center" mx="2">
|
||||||
<Switch id="replies" mr="2" isChecked={showReplies} onChange={toggleReplies} />
|
<Switch id="replies" mr="2" isChecked={showReplies} onChange={toggleReplies} />
|
||||||
<FormLabel htmlFor="replies" mb="0">
|
<FormLabel htmlFor="replies" mb="0">
|
||||||
@ -48,20 +67,15 @@ const UserNotesTab = () => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<RelayIconStack ml="auto" relays={readRelays} direction="row-reverse" mr="4" maxRelays={4} />
|
<RelayIconStack ml="auto" relays={readRelays} direction="row-reverse" mr="4" maxRelays={4} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{filteredEvents.map((event) =>
|
{eventsTimeline.map((event) =>
|
||||||
event.kind === 6 ? (
|
event.kind === 6 ? (
|
||||||
<RepostNote key={event.id} event={event} maxHeight={1200} />
|
<RepostNote key={event.id} event={event} maxHeight={1200} />
|
||||||
) : (
|
) : (
|
||||||
<Note key={event.id} event={event} maxHeight={1200} />
|
<Note key={event.id} event={event} maxHeight={1200} />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{loading ? (
|
|
||||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
<LoadMoreButton timeline={timeline} />
|
||||||
) : (
|
|
||||||
<Button onClick={() => timeline.loadMore()} flexShrink={0}>
|
|
||||||
Load More
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Button, Flex, Spinner, Text } from "@chakra-ui/react";
|
import { Flex, Text } from "@chakra-ui/react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { NoteLink } from "../../components/note-link";
|
import { NoteLink } from "../../components/note-link";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
@ -6,6 +6,7 @@ import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr-event"
|
|||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
function ReportEvent({ report }: { report: NostrEvent }) {
|
function ReportEvent({ report }: { report: NostrEvent }) {
|
||||||
const reportedEvent = report.tags.filter(isETag)[0]?.[1];
|
const reportedEvent = report.tags.filter(isETag)[0]?.[1];
|
||||||
@ -37,29 +38,18 @@ export default function UserReportsTab() {
|
|||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
const contextRelays = useAdditionalRelayContext();
|
const contextRelays = useAdditionalRelayContext();
|
||||||
|
|
||||||
const {
|
const { timeline, loader } = useTimelineLoader(`${truncatedId(pubkey)}-reports`, contextRelays, {
|
||||||
events: reports,
|
authors: [pubkey],
|
||||||
loading,
|
kinds: [1984],
|
||||||
loadMore,
|
});
|
||||||
} = useTimelineLoader(
|
|
||||||
`${truncatedId(pubkey)}-reports`,
|
|
||||||
contextRelays,
|
|
||||||
{ authors: [pubkey], kinds: [1984] },
|
|
||||||
{ pageSize: 60 * 60 * 24 * 7 }
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||||
{reports.map((report) => (
|
{timeline.map((report) => (
|
||||||
<ReportEvent key={report.id} report={report} />
|
<ReportEvent key={report.id} report={report} />
|
||||||
))}
|
))}
|
||||||
{loading ? (
|
|
||||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
<LoadMoreButton timeline={loader} />
|
||||||
) : (
|
|
||||||
<Button onClick={() => loadMore()} flexShrink={0}>
|
|
||||||
Load More
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Box, Button, Flex, Select, Spinner, Text, useDisclosure } from "@chakra-ui/react";
|
import { Box, Button, Flex, Select, Text, useDisclosure } from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
|
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
|
||||||
import { LightningIcon } from "../../components/icons";
|
import { LightningIcon } from "../../components/icons";
|
||||||
@ -9,16 +9,17 @@ import { UserAvatarLink } from "../../components/user-avatar-link";
|
|||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
import { truncatedId } from "../../helpers/nostr-event";
|
||||||
import { isProfileZap, isNoteZap, parseZapNote, totalZaps } from "../../helpers/zaps";
|
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||||
|
import LoadMoreButton from "../../components/load-more-button";
|
||||||
|
|
||||||
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||||
const { isOpen, onToggle } = useDisclosure();
|
const { isOpen, onToggle } = useDisclosure();
|
||||||
try {
|
try {
|
||||||
const { request, payment, eventId } = parseZapNote(zapEvent);
|
const { request, payment, eventId } = parseZapEvent(zapEvent);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -68,16 +69,26 @@ const UserZapsTab = () => {
|
|||||||
const contextRelays = useAdditionalRelayContext();
|
const contextRelays = useAdditionalRelayContext();
|
||||||
const relays = useReadRelayUrls(contextRelays);
|
const relays = useReadRelayUrls(contextRelays);
|
||||||
|
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const eventFilter = useCallback(
|
||||||
|
(event: NostrEvent) => {
|
||||||
|
switch (filter) {
|
||||||
|
case "note":
|
||||||
|
return isNoteZap(event);
|
||||||
|
case "profile":
|
||||||
|
return isProfileZap(event);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[filter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { timeline, loader } = useTimelineLoader(
|
||||||
`${truncatedId(pubkey)}-zaps`,
|
`${truncatedId(pubkey)}-zaps`,
|
||||||
relays,
|
relays,
|
||||||
{ "#p": [pubkey], kinds: [9735] },
|
{ "#p": [pubkey], kinds: [9735] },
|
||||||
{ pageSize: 60 * 60 * 24 * 7 }
|
{ eventFilter }
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeline =
|
|
||||||
filter === "note" ? events.filter(isNoteZap) : filter === "profile" ? events.filter(isProfileZap) : events;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2" p="2" pb="8" h="full" overflowY="auto">
|
<Flex direction="column" gap="2" p="2" pb="8" h="full" overflowY="auto">
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
@ -101,13 +112,8 @@ const UserZapsTab = () => {
|
|||||||
<Zap zapEvent={event} />
|
<Zap zapEvent={event} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
))}
|
))}
|
||||||
{loading ? (
|
|
||||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
<LoadMoreButton timeline={loader} />
|
||||||
) : (
|
|
||||||
<Button onClick={() => loadMore()} flexShrink={0}>
|
|
||||||
Load More
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user