mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-06 02:48:33 +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 ProfileView from "./views/profile";
|
||||
import FollowingTab from "./views/home/following-tab";
|
||||
import DiscoverTab from "./views/home/discover-tab";
|
||||
import GlobalTab from "./views/home/global-tab";
|
||||
import HashTagView from "./views/hashtag";
|
||||
import UserView from "./views/user";
|
||||
@ -102,7 +101,6 @@ const router = createHashRouter([
|
||||
children: [
|
||||
{ path: "", element: <FollowingTab /> },
|
||||
{ path: "following", element: <FollowingTab /> },
|
||||
{ path: "discover", element: <DiscoverTab /> },
|
||||
{ path: "global", element: <GlobalTab /> },
|
||||
],
|
||||
},
|
||||
|
@ -4,84 +4,210 @@ 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 Subject, { PersistentSubject } from "./subject";
|
||||
|
||||
type Options = {
|
||||
name?: string;
|
||||
pageSize: number;
|
||||
startLimit: number;
|
||||
};
|
||||
export type TimelineLoaderOptions = Partial<Options>;
|
||||
const BLOCK_SIZE = 10;
|
||||
|
||||
export class TimelineLoader {
|
||||
relays: string[];
|
||||
type EventFilter = (event: NostrEvent) => boolean;
|
||||
|
||||
class RelayTimelineLoader {
|
||||
relay: string;
|
||||
query: NostrQuery;
|
||||
events = new PersistentSubject<NostrEvent[]>([]);
|
||||
loading = new PersistentSubject(false);
|
||||
page = new PersistentSubject(0);
|
||||
blockSize = BLOCK_SIZE;
|
||||
|
||||
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 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 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;
|
||||
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.createLoaders();
|
||||
}
|
||||
|
||||
setQuery(query: NostrQuery) {
|
||||
this.query = { ...query, limit: this.opts.startLimit };
|
||||
this.subscription.setQuery(this.query);
|
||||
}
|
||||
|
||||
setRelays(relays: string[]) {
|
||||
this.relays = relays;
|
||||
this.subscription.setRelays(relays);
|
||||
}
|
||||
|
||||
private seenEvents = new Set<string>();
|
||||
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);
|
||||
|
||||
if (!this.eventFilter || this.eventFilter(event)) {
|
||||
this.timeline.next(utils.insertEventIntoDescendingList(Array.from(this.timeline.value), event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPageDates(page: number) {
|
||||
const start = this.events.value[0]?.created_at ?? dayjs().unix();
|
||||
const until = start - page * this.opts.pageSize;
|
||||
const since = until - this.opts.pageSize;
|
||||
|
||||
return {
|
||||
until,
|
||||
since,
|
||||
};
|
||||
private createLoaders() {
|
||||
for (const relay of this.relays) {
|
||||
if (!this.relayTimelineLoaders.has(relay)) {
|
||||
const loader = new RelayTimelineLoader(relay, this.query);
|
||||
this.relayTimelineLoaders.set(relay, loader);
|
||||
loader.onEvent.subscribe(this.handleEvent, this);
|
||||
loader.onBlockFinish.subscribe(this.updateLoading, this);
|
||||
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() {
|
||||
if (this.loading.value) return;
|
||||
setRelays(relays: string[]) {
|
||||
// remove loaders
|
||||
this.removeLoaders((loader) => !relays.includes(loader.relay));
|
||||
|
||||
const query = { ...this.query, ...this.getPageDates(this.page.value) };
|
||||
const request = new NostrRequest(this.relays);
|
||||
request.onEvent.subscribe(this.handleEvent, this);
|
||||
request.onComplete.then(() => {
|
||||
this.loading.next(false);
|
||||
});
|
||||
request.start(query);
|
||||
this.relays = relays;
|
||||
this.createLoaders();
|
||||
|
||||
this.loading.next(true);
|
||||
this.page.next(this.page.value + 1);
|
||||
this.subscription.setRelays(relays);
|
||||
}
|
||||
setQuery(query: NostrQuery) {
|
||||
this.removeLoaders();
|
||||
|
||||
forgetEvents() {
|
||||
this.query = query;
|
||||
this.events.next([]);
|
||||
this.timeline.next([]);
|
||||
this.seenEvents.clear();
|
||||
|
||||
this.createLoaders();
|
||||
|
||||
// update the subscription
|
||||
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() {
|
||||
this.subscription.open();
|
||||
@ -89,4 +215,13 @@ export class TimelineLoader {
|
||||
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 { TimelineLoader } from "./timeline-loader";
|
||||
|
||||
const PAGE_SIZE = 60 * 60 * 24 * 7; //in seconds
|
||||
|
||||
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;
|
||||
|
||||
export default class UserTimeline extends TimelineLoader {
|
||||
constructor(pubkey: string) {
|
||||
this.pubkey = pubkey;
|
||||
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();
|
||||
super([], { authors: [pubkey], kinds: [1, 6] }, truncatedId(pubkey) + "-timeline");
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNostrEvent) {
|
||||
return embedJSX(content, {
|
||||
regexp: /:([a-zA-Z0-9]+):/i,
|
||||
regexp: /:([a-zA-Z0-9_]+):/i,
|
||||
render: (match) => {
|
||||
const emojiTag = note.tags.find(
|
||||
(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 { useMemo } from "react";
|
||||
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 useEventZaps from "../../hooks/use-event-zaps";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
@ -19,7 +19,7 @@ export default function NoteZapButton({ note, ...props }: { note: NostrEvent } &
|
||||
const parsed = [];
|
||||
for (const zap of zaps) {
|
||||
try {
|
||||
parsed.push(parseZapNote(zap));
|
||||
parsed.push(parseZapEvent(zap));
|
||||
} catch (e) {}
|
||||
}
|
||||
return parsed;
|
||||
|
@ -17,7 +17,7 @@ import { UserAvatarLink } from "../user-avatar-link";
|
||||
import { UserLink } from "../user-link";
|
||||
import dayjs from "dayjs";
|
||||
import { DislikeIcon, LightningIcon, LikeIcon } from "../icons";
|
||||
import { parseZapNote } from "../../helpers/zaps";
|
||||
import { parseZapEvent } from "../../helpers/zaps";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import useEventReactions from "../../hooks/use-event-reactions";
|
||||
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 isMobile = useIsMobile();
|
||||
try {
|
||||
const { payment, request } = parseZapNote(event);
|
||||
const { payment, request } = parseZapEvent(event);
|
||||
|
||||
if (!payment.amount) return null;
|
||||
|
||||
|
@ -55,7 +55,7 @@ export function totalZaps(events: NostrEvent[]) {
|
||||
return total;
|
||||
}
|
||||
|
||||
export function parseZapNote(event: NostrEvent) {
|
||||
function parseZapEvent(event: NostrEvent) {
|
||||
const zapRequestStr = event.tags.find(([t, v]) => t === "description")?.[1];
|
||||
if (!zapRequestStr) throw new Error("no description tag");
|
||||
|
||||
@ -77,3 +77,14 @@ export function parseZapNote(event: NostrEvent) {
|
||||
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 { TimelineLoader, TimelineLoaderOptions } from "../classes/timeline-loader";
|
||||
import { TimelineLoader } from "../classes/timeline-loader";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import useSubject from "./use-subject";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
|
||||
type Options = TimelineLoaderOptions & {
|
||||
type Options = {
|
||||
enabled?: boolean;
|
||||
eventFilter?: (event: NostrEvent) => boolean;
|
||||
cursor?: number;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function useTimelineLoader(key: string, relays: string[], query: NostrQuery, opts?: Options) {
|
||||
if (opts && !opts.name) opts.name = key;
|
||||
|
||||
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(() => {
|
||||
loader.forgetEvents();
|
||||
loader.setQuery(query);
|
||||
}, [key]);
|
||||
|
||||
}, [JSON.stringify(query)]);
|
||||
useEffect(() => {
|
||||
loader.setRelays(relays);
|
||||
}, [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;
|
||||
useEffect(() => {
|
||||
@ -35,17 +45,14 @@ export function useTimelineLoader(key: string, relays: string[], query: NostrQue
|
||||
loader.close();
|
||||
});
|
||||
|
||||
const events = useSubject(loader.events);
|
||||
const timeline = useSubject(loader.timeline);
|
||||
const loading = useSubject(loader.loading);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (enabled) loader.loadMore();
|
||||
}, [enabled]);
|
||||
const complete = useSubject(loader.complete);
|
||||
|
||||
return {
|
||||
loader,
|
||||
events,
|
||||
timeline,
|
||||
loading,
|
||||
loadMore,
|
||||
complete,
|
||||
};
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ export function mediaSetup(ogObject: OgObjectInteral) {
|
||||
fields
|
||||
.filter((item) => item.multiple && item.fieldName && item.fieldName.match("(ogImage|ogVideo|twitter|musicSong).*"))
|
||||
.forEach((item) => {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
delete ogObject[item.fieldName];
|
||||
});
|
||||
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Spinner,
|
||||
Switch,
|
||||
useDisclosure,
|
||||
useEditableControls,
|
||||
@ -22,8 +21,10 @@ import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import { Note } from "../../components/note";
|
||||
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 { NostrEvent } from "../../types/nostr-event";
|
||||
import LoadMoreButton from "../../components/load-more-button";
|
||||
|
||||
function EditableControls() {
|
||||
const { isEditing, getSubmitButtonProps, getCancelButtonProps, getEditButtonProps } = useEditableControls();
|
||||
@ -51,18 +52,23 @@ export default function HashTagView() {
|
||||
|
||||
const relaysModal = 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`,
|
||||
selectedRelays,
|
||||
{ kinds: [1], "#t": [hashtag] },
|
||||
{ pageSize: 60 * 10 }
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
||||
|
||||
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">
|
||||
<Editable
|
||||
value={editableHashtag}
|
||||
@ -95,13 +101,8 @@ export default function HashTagView() {
|
||||
{timeline.map((event) => (
|
||||
<Note key={event.id} event={event} maxHeight={600} />
|
||||
))}
|
||||
{loading ? (
|
||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
||||
) : (
|
||||
<Button onClick={() => loadMore()} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<LoadMoreButton timeline={loader} />
|
||||
</Flex>
|
||||
|
||||
{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 { useSearchParams } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import { useInterval } from "react-use";
|
||||
import { Note } from "../../components/note";
|
||||
import { isReply, truncatedId } from "../../helpers/nostr-event";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import { AddIcon } from "@chakra-ui/icons";
|
||||
import { useContext } from "react";
|
||||
import { useCallback, useContext, useRef } from "react";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import RepostNote from "../../components/repost-note";
|
||||
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() {
|
||||
const account = useCurrentAccount()!;
|
||||
@ -24,19 +27,38 @@ function FollowingTabBody() {
|
||||
showReplies ? setSearch({}) : setSearch({ replies: "show" });
|
||||
};
|
||||
|
||||
const following = contacts?.contacts || [];
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${truncatedId(account.pubkey)}-following-posts`,
|
||||
readRelays,
|
||||
{ authors: following, kinds: [1, 6], since: dayjs().subtract(2, "hour").unix() },
|
||||
{ pageSize: 60 * 60, enabled: following.length > 0 }
|
||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||
const scrollPosition = useScrollPosition(scrollBox);
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
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 (
|
||||
<Flex direction="column" gap="2">
|
||||
<Button variant="outline" leftIcon={<AddIcon />} onClick={() => openModal()} isDisabled={account.readonly}>
|
||||
<Flex py="4" direction="column" gap="2" overflowY="auto" overflowX="hidden" ref={scrollBox}>
|
||||
<Button
|
||||
variant="outline"
|
||||
leftIcon={<AddIcon />}
|
||||
onClick={() => openModal()}
|
||||
isDisabled={account.readonly}
|
||||
flexShrink={0}
|
||||
>
|
||||
New Post
|
||||
</Button>
|
||||
<FormControl display="flex" alignItems="center">
|
||||
@ -52,7 +74,8 @@ function FollowingTabBody() {
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -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 { Note } from "../../components/note";
|
||||
import { unique } from "../../helpers/array";
|
||||
@ -6,6 +7,8 @@ import { isReply } from "../../helpers/nostr-event";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import LoadMoreButton from "../../components/load-more-button";
|
||||
|
||||
export default function GlobalTab() {
|
||||
useAppTitle("global");
|
||||
@ -17,21 +20,27 @@ export default function GlobalTab() {
|
||||
setSearchParams({ relay: url });
|
||||
} else setSearchParams({});
|
||||
};
|
||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||
|
||||
const availableRelays = unique([...defaultRelays, selectedRelay]).filter(Boolean);
|
||||
|
||||
const { isOpen: showReplies, onToggle } = useDisclosure();
|
||||
const { events, loading, loadMore, loader } = useTimelineLoader(
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (!showReplies && isReply(event)) return false;
|
||||
return true;
|
||||
},
|
||||
[showReplies]
|
||||
);
|
||||
|
||||
const { timeline, loader } = useTimelineLoader(
|
||||
`global`,
|
||||
selectedRelay ? [selectedRelay] : [],
|
||||
{ kinds: [1] },
|
||||
{ pageSize: 60*10 }
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
const timeline = showReplies ? events : events.filter((e) => !isReply(e));
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Flex py="4" direction="column" gap="2" overflowY="auto" overflowX="hidden">
|
||||
<Flex gap="2">
|
||||
<Select
|
||||
placeholder="Select Relay"
|
||||
@ -39,7 +48,6 @@ export default function GlobalTab() {
|
||||
value={selectedRelay}
|
||||
onChange={(e) => {
|
||||
setSelectedRelay(e.target.value);
|
||||
loader.forgetEvents();
|
||||
}}
|
||||
>
|
||||
{availableRelays.map((url) => (
|
||||
@ -58,7 +66,8 @@ export default function GlobalTab() {
|
||||
{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>}
|
||||
|
||||
<LoadMoreButton timeline={loader} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Outlet, useMatches, useNavigate } from "react-router-dom";
|
||||
|
||||
const tabs = [
|
||||
{ label: "Following", path: "/following" },
|
||||
{ label: "Discover", path: "/discover" },
|
||||
// { label: "Discover", path: "/discover" },
|
||||
// { label: "Popular", path: "/popular" },
|
||||
{ label: "Global", path: "/global" },
|
||||
];
|
||||
@ -30,9 +30,9 @@ export default function HomeView() {
|
||||
<Tab key={label}>{label}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels overflow="auto" height="100%">
|
||||
<TabPanels overflow="hidden" h="full">
|
||||
{tabs.map(({ label }) => (
|
||||
<TabPanel key={label} pr={0} pl={0}>
|
||||
<TabPanel key={label} p={0} overflow="hidden" h="full" display="flex" flexDirection="column">
|
||||
<Outlet />
|
||||
</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 { memo } from "react";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
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 { NoteLink } from "../../components/note-link";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import LoadMoreButton from "../../components/load-more-button";
|
||||
|
||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||
<Card size="sm" variant="outline">
|
||||
@ -37,26 +38,25 @@ const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
||||
function NotificationsPage() {
|
||||
const readRelays = useReadRelayUrls();
|
||||
const account = useCurrentAccount()!;
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
|
||||
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
|
||||
const { timeline, loader } = useTimelineLoader(
|
||||
"notifications",
|
||||
readRelays,
|
||||
{
|
||||
"#p": [account.pubkey],
|
||||
kinds: [1],
|
||||
},
|
||||
{ pageSize: 60 * 60 * 24 }
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
const timeline = events
|
||||
// ignore events made my the user
|
||||
.filter((e) => e.pubkey !== account.pubkey);
|
||||
|
||||
return (
|
||||
<Flex direction="column" overflowX="hidden" overflowY="auto" gap="2">
|
||||
{timeline.map((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>
|
||||
);
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ export default function UserAboutTab() {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
@ -170,9 +170,7 @@ export default function UserAboutTab() {
|
||||
<Stat>
|
||||
<StatLabel>Following</StatLabel>
|
||||
<StatNumber>{contacts ? readablizeSats(contacts.contacts.length) : "Unknown"}</StatNumber>
|
||||
{contacts && (
|
||||
<StatHelpText>Updated {dayjs.unix(contacts.created_at).fromNow()}</StatHelpText>
|
||||
)}
|
||||
{contacts && <StatHelpText>Updated {dayjs.unix(contacts.created_at).fromNow()}</StatHelpText>}
|
||||
</Stat>
|
||||
|
||||
{stats && (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Box, Button, Flex, Grid, IconButton, Spinner } from "@chakra-ui/react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Box, Flex, Grid, IconButton } from "@chakra-ui/react";
|
||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||
import { useMount, useUnmount } from "react-use";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
@ -10,6 +10,8 @@ import { ExternalLinkIcon } from "../../components/icons";
|
||||
import { getSharableNoteId } from "../../helpers/nip19";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
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");
|
||||
|
||||
@ -21,10 +23,13 @@ const UserMediaTab = () => {
|
||||
|
||||
const timeline = useMemo(() => userTimelineService.getTimeline(pubkey), [pubkey]);
|
||||
|
||||
const events = useSubject(timeline.events);
|
||||
const loading = useSubject(timeline.loading);
|
||||
const eventFilter = useCallback((e: NostrEvent) => e.kind === 1 && !!e.content.match(matchAllImages), []);
|
||||
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(() => {
|
||||
timeline.setRelays(contextRelays);
|
||||
@ -36,7 +41,7 @@ const UserMediaTab = () => {
|
||||
const images = useMemo(() => {
|
||||
var images: { eventId: string; src: string; index: number }[] = [];
|
||||
|
||||
for (const event of filteredEvents) {
|
||||
for (const event of events) {
|
||||
const urls = event.content.matchAll(matchAllImages);
|
||||
|
||||
let i = 0;
|
||||
@ -46,7 +51,7 @@ const UserMediaTab = () => {
|
||||
}
|
||||
|
||||
return images;
|
||||
}, [filteredEvents]);
|
||||
}, [events]);
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" px="2" pb="8" h="full" overflowY="auto">
|
||||
@ -77,13 +82,8 @@ const UserMediaTab = () => {
|
||||
))}
|
||||
</Grid>
|
||||
</ImageGalleryProvider>
|
||||
{loading ? (
|
||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
||||
) : (
|
||||
<Button onClick={() => timeline.loadMore()} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<LoadMoreButton timeline={timeline} />
|
||||
</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 { Note } from "../../components/note";
|
||||
import RepostNote from "../../components/repost-note";
|
||||
import { isReply, isRepost } from "../../helpers/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
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 { useMount, useUnmount } from "react-use";
|
||||
import { useInterval, useMount, useUnmount } from "react-use";
|
||||
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 { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -17,11 +20,21 @@ const UserNotesTab = () => {
|
||||
const { isOpen: showReplies, onToggle: toggleReplies } = 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 events = useSubject(timeline.events);
|
||||
const loading = useSubject(timeline.loading);
|
||||
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (!showReplies && isReply(event)) return false;
|
||||
if (hideReposts && isRepost(event)) return false;
|
||||
return true;
|
||||
},
|
||||
[showReplies, hideReposts]
|
||||
);
|
||||
useEffect(() => {
|
||||
timeline.setFilter(eventFilter);
|
||||
}, [timeline, eventFilter]);
|
||||
useEffect(() => {
|
||||
timeline.setRelays(readRelays);
|
||||
}, [timeline, readRelays.join("|")]);
|
||||
@ -29,14 +42,20 @@ const UserNotesTab = () => {
|
||||
useMount(() => timeline.open());
|
||||
useUnmount(() => timeline.close());
|
||||
|
||||
const filteredEvents = events.filter((event) => {
|
||||
if (!showReplies && isReply(event)) return false;
|
||||
if (hideReposts && isRepost(event)) return false;
|
||||
return true;
|
||||
});
|
||||
useInterval(() => {
|
||||
const events = timeline.timeline.value;
|
||||
if (events.length > 0) {
|
||||
const eventAtScrollPos = events[Math.floor(scrollPosition * (events.length - 1))];
|
||||
timeline.setCursor(eventAtScrollPos.created_at);
|
||||
}
|
||||
|
||||
timeline.loadNextBlocks();
|
||||
}, 1000);
|
||||
|
||||
const eventsTimeline = useSubject(timeline.timeline);
|
||||
|
||||
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">
|
||||
<Switch id="replies" mr="2" isChecked={showReplies} onChange={toggleReplies} />
|
||||
<FormLabel htmlFor="replies" mb="0">
|
||||
@ -48,20 +67,15 @@ const UserNotesTab = () => {
|
||||
</FormLabel>
|
||||
<RelayIconStack ml="auto" relays={readRelays} direction="row-reverse" mr="4" maxRelays={4} />
|
||||
</FormControl>
|
||||
{filteredEvents.map((event) =>
|
||||
{eventsTimeline.map((event) =>
|
||||
event.kind === 6 ? (
|
||||
<RepostNote 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} />
|
||||
) : (
|
||||
<Button onClick={() => timeline.loadMore()} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<LoadMoreButton timeline={timeline} />
|
||||
</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 { NoteLink } from "../../components/note-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 { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import LoadMoreButton from "../../components/load-more-button";
|
||||
|
||||
function ReportEvent({ report }: { report: NostrEvent }) {
|
||||
const reportedEvent = report.tags.filter(isETag)[0]?.[1];
|
||||
@ -37,29 +38,18 @@ export default function UserReportsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
|
||||
const {
|
||||
events: reports,
|
||||
loading,
|
||||
loadMore,
|
||||
} = useTimelineLoader(
|
||||
`${truncatedId(pubkey)}-reports`,
|
||||
contextRelays,
|
||||
{ authors: [pubkey], kinds: [1984] },
|
||||
{ pageSize: 60 * 60 * 24 * 7 }
|
||||
);
|
||||
const { timeline, loader } = useTimelineLoader(`${truncatedId(pubkey)}-reports`, contextRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [1984],
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" pr="2" pl="2">
|
||||
{reports.map((report) => (
|
||||
{timeline.map((report) => (
|
||||
<ReportEvent key={report.id} report={report} />
|
||||
))}
|
||||
{loading ? (
|
||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
||||
) : (
|
||||
<Button onClick={() => loadMore()} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<LoadMoreButton timeline={loader} />
|
||||
</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 { useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { ErrorBoundary, ErrorFallback } from "../../components/error-boundary";
|
||||
import { LightningIcon } from "../../components/icons";
|
||||
@ -9,16 +9,17 @@ import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
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 { NostrEvent } from "../../types/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import LoadMoreButton from "../../components/load-more-button";
|
||||
|
||||
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
try {
|
||||
const { request, payment, eventId } = parseZapNote(zapEvent);
|
||||
const { request, payment, eventId } = parseZapEvent(zapEvent);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -68,16 +69,26 @@ const UserZapsTab = () => {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
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`,
|
||||
relays,
|
||||
{ "#p": [pubkey], kinds: [9735] },
|
||||
{ pageSize: 60 * 60 * 24 * 7 }
|
||||
{ eventFilter }
|
||||
);
|
||||
|
||||
const timeline =
|
||||
filter === "note" ? events.filter(isNoteZap) : filter === "profile" ? events.filter(isProfileZap) : events;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2" p="2" pb="8" h="full" overflowY="auto">
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
@ -101,13 +112,8 @@ const UserZapsTab = () => {
|
||||
<Zap zapEvent={event} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
{loading ? (
|
||||
<Spinner ml="auto" mr="auto" mt="8" mb="8" flexShrink={0} />
|
||||
) : (
|
||||
<Button onClick={() => loadMore()} flexShrink={0}>
|
||||
Load More
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<LoadMoreButton timeline={loader} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user