bug: load more fix
This commit is contained in:
parent
465c59ea20
commit
2ccf593476
@ -5,7 +5,7 @@ import { useInView } from "react-intersection-observer";
|
|||||||
import { TaggedRawEvent, EventKind, u256 } from "@snort/nostr";
|
import { TaggedRawEvent, EventKind, u256 } from "@snort/nostr";
|
||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import { dedupeByPubkey, findTag, tagFilterOfTextRepost, unixNow } from "Util";
|
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "Util";
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
|
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
|
||||||
import LoadMore from "Element/LoadMore";
|
import LoadMore from "Element/LoadMore";
|
||||||
|
@ -121,8 +121,8 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// clear store if changing relays
|
// clear store if changing relays
|
||||||
main.store.clear();
|
main.clear();
|
||||||
latest.store.clear();
|
latest.clear();
|
||||||
}, [options.relay]);
|
}, [options.relay]);
|
||||||
|
|
||||||
const subNext = useMemo(() => {
|
const subNext = useMemo(() => {
|
||||||
@ -180,7 +180,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
main: main.data,
|
main: main.data,
|
||||||
related: related.data,
|
related: related.data,
|
||||||
latest: latest.data,
|
latest: latest.data,
|
||||||
loading: main.store.loading,
|
loading: main.loading(),
|
||||||
loadMore: () => {
|
loadMore: () => {
|
||||||
if (main.data) {
|
if (main.data) {
|
||||||
console.debug("Timeline load more!");
|
console.debug("Timeline load more!");
|
||||||
@ -194,8 +194,8 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
|||||||
},
|
},
|
||||||
showLatest: () => {
|
showLatest: () => {
|
||||||
if (latest.data) {
|
if (latest.data) {
|
||||||
main.store.add(latest.data);
|
main.add(latest.data);
|
||||||
latest.store.clear();
|
latest.clear();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
import { RequestBuilder, System } from "System";
|
import { RequestBuilder, System } from "System";
|
||||||
import { FlatNoteStore, NoteStore, StoreSnapshot } from "System/NoteCollection";
|
import { EmptySnapshot, NoteStore, StoreSnapshot } from "System/NoteCollection";
|
||||||
import { unwrap } from "Util";
|
import { unwrap } from "Util";
|
||||||
|
|
||||||
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
|
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
|
||||||
@ -28,10 +28,6 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
|
|||||||
release();
|
release();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const emptyStore = {
|
|
||||||
data: undefined,
|
|
||||||
store: new FlatNoteStore(),
|
|
||||||
} as StoreSnapshot<TSnapshot>;
|
|
||||||
const getState = (): StoreSnapshot<TSnapshot> => {
|
const getState = (): StoreSnapshot<TSnapshot> => {
|
||||||
if (rb?.id) {
|
if (rb?.id) {
|
||||||
const feed = System.GetFeed(rb.id);
|
const feed = System.GetFeed(rb.id);
|
||||||
@ -39,7 +35,7 @@ const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TSto
|
|||||||
return unwrap(feed).snapshot as StoreSnapshot<TSnapshot>;
|
return unwrap(feed).snapshot as StoreSnapshot<TSnapshot>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return emptyStore;
|
return EmptySnapshot as StoreSnapshot<TSnapshot>;
|
||||||
};
|
};
|
||||||
return useSyncExternalStore<StoreSnapshot<TSnapshot>>(
|
return useSyncExternalStore<StoreSnapshot<TSnapshot>>(
|
||||||
v => subscribe(v),
|
v => subscribe(v),
|
||||||
|
@ -3,9 +3,22 @@ import { findTag } from "Util";
|
|||||||
|
|
||||||
export interface StoreSnapshot<TSnapshot> {
|
export interface StoreSnapshot<TSnapshot> {
|
||||||
data: TSnapshot | undefined;
|
data: TSnapshot | undefined;
|
||||||
store: NoteStore;
|
clear: () => void;
|
||||||
|
loading: () => boolean;
|
||||||
|
add: (ev: Readonly<TaggedRawEvent> | Readonly<Array<TaggedRawEvent>>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmptySnapshot = {
|
||||||
|
data: undefined,
|
||||||
|
clear: () => {
|
||||||
|
// empty
|
||||||
|
},
|
||||||
|
loading: () => true,
|
||||||
|
add: (ev: Readonly<TaggedRawEvent> | Readonly<Array<TaggedRawEvent>>) => {
|
||||||
|
// empty
|
||||||
|
},
|
||||||
|
} as StoreSnapshot<FlatNoteStore>;
|
||||||
|
|
||||||
export type NoteStoreSnapshotData = Readonly<Array<TaggedRawEvent>> | Readonly<TaggedRawEvent>;
|
export type NoteStoreSnapshotData = Readonly<Array<TaggedRawEvent>> | Readonly<TaggedRawEvent>;
|
||||||
export type NoteStoreHook = () => void;
|
export type NoteStoreHook = () => void;
|
||||||
export type NoteStoreHookRelease = () => void;
|
export type NoteStoreHookRelease = () => void;
|
||||||
@ -20,7 +33,6 @@ export type OnEoseCallbackRelease = () => void;
|
|||||||
export abstract class NoteStore {
|
export abstract class NoteStore {
|
||||||
abstract add(ev: Readonly<TaggedRawEvent> | Readonly<Array<TaggedRawEvent>>): void;
|
abstract add(ev: Readonly<TaggedRawEvent> | Readonly<Array<TaggedRawEvent>>): void;
|
||||||
abstract clear(): void;
|
abstract clear(): void;
|
||||||
abstract eose(c: string): void;
|
|
||||||
|
|
||||||
// react hooks
|
// react hooks
|
||||||
abstract hook(cb: NoteStoreHook): NoteStoreHookRelease;
|
abstract hook(cb: NoteStoreHook): NoteStoreHookRelease;
|
||||||
@ -28,7 +40,6 @@ export abstract class NoteStore {
|
|||||||
|
|
||||||
// events
|
// events
|
||||||
abstract onEvent(cb: OnEventCallback): OnEventCallbackRelease;
|
abstract onEvent(cb: OnEventCallback): OnEventCallbackRelease;
|
||||||
abstract onEose(cb: OnEoseCallback): OnEoseCallback;
|
|
||||||
|
|
||||||
abstract get snapshot(): StoreSnapshot<NoteStoreSnapshotData>;
|
abstract get snapshot(): StoreSnapshot<NoteStoreSnapshotData>;
|
||||||
abstract get loading(): boolean;
|
abstract get loading(): boolean;
|
||||||
@ -38,10 +49,11 @@ export abstract class NoteStore {
|
|||||||
export abstract class HookedNoteStore<TSnapshot extends NoteStoreSnapshotData> implements NoteStore {
|
export abstract class HookedNoteStore<TSnapshot extends NoteStoreSnapshotData> implements NoteStore {
|
||||||
#hooks: Array<NoteStoreHook> = [];
|
#hooks: Array<NoteStoreHook> = [];
|
||||||
#eventHooks: Array<OnEventCallback> = [];
|
#eventHooks: Array<OnEventCallback> = [];
|
||||||
#eoseHooks: Array<OnEoseCallback> = [];
|
|
||||||
#loading = true;
|
#loading = true;
|
||||||
#storeSnapshot: StoreSnapshot<TSnapshot> = {
|
#storeSnapshot: StoreSnapshot<TSnapshot> = {
|
||||||
store: this,
|
clear: () => this.clear(),
|
||||||
|
loading: () => this.loading,
|
||||||
|
add: ev => this.add(ev),
|
||||||
data: undefined,
|
data: undefined,
|
||||||
};
|
};
|
||||||
#needsSnapshot = true;
|
#needsSnapshot = true;
|
||||||
@ -60,15 +72,9 @@ export abstract class HookedNoteStore<TSnapshot extends NoteStoreSnapshotData> i
|
|||||||
this.onChange([]);
|
this.onChange([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract add(ev: TaggedRawEvent | Array<TaggedRawEvent>): void;
|
abstract add(ev: Readonly<TaggedRawEvent> | Readonly<Array<TaggedRawEvent>>): void;
|
||||||
abstract clear(): void;
|
abstract clear(): void;
|
||||||
|
|
||||||
eose(c: string): void {
|
|
||||||
for (const hkE of this.#eoseHooks) {
|
|
||||||
hkE(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hook(cb: NoteStoreHook): NoteStoreHookRelease {
|
hook(cb: NoteStoreHook): NoteStoreHookRelease {
|
||||||
this.#hooks.push(cb);
|
this.#hooks.push(cb);
|
||||||
return () => {
|
return () => {
|
||||||
@ -90,14 +96,6 @@ export abstract class HookedNoteStore<TSnapshot extends NoteStoreSnapshotData> i
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onEose(cb: OnEoseCallback): OnEoseCallback {
|
|
||||||
this.#eoseHooks.push(cb);
|
|
||||||
return () => {
|
|
||||||
const idx = this.#eoseHooks.findIndex(a => a === cb);
|
|
||||||
this.#eoseHooks.splice(idx, 1);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract takeSnapshot(): TSnapshot | undefined;
|
protected abstract takeSnapshot(): TSnapshot | undefined;
|
||||||
|
|
||||||
protected onChange(changes: Readonly<Array<TaggedRawEvent>>): void {
|
protected onChange(changes: Readonly<Array<TaggedRawEvent>>): void {
|
||||||
@ -115,8 +113,8 @@ export abstract class HookedNoteStore<TSnapshot extends NoteStoreSnapshotData> i
|
|||||||
#updateSnapshot() {
|
#updateSnapshot() {
|
||||||
if (this.#needsSnapshot) {
|
if (this.#needsSnapshot) {
|
||||||
this.#storeSnapshot = {
|
this.#storeSnapshot = {
|
||||||
|
...this.#storeSnapshot,
|
||||||
data: this.takeSnapshot(),
|
data: this.takeSnapshot(),
|
||||||
store: this,
|
|
||||||
};
|
};
|
||||||
this.#needsSnapshot = false;
|
this.#needsSnapshot = false;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,11 @@ export class Query {
|
|||||||
*/
|
*/
|
||||||
#sentToRelays: Array<Readonly<Connection>> = [];
|
#sentToRelays: Array<Readonly<Connection>> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When each relay returned EOSE
|
||||||
|
*/
|
||||||
|
#eoseRelays: Map<string, number> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leave the query open until its removed
|
* Leave the query open until its removed
|
||||||
*/
|
*/
|
||||||
@ -88,4 +93,32 @@ export class Query {
|
|||||||
c.CloseReq(this.id);
|
c.CloseReq(this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eose(sub: string, relay: string) {
|
||||||
|
if (sub === this.id) {
|
||||||
|
console.debug(`[EOSE][${sub}] ${relay}`);
|
||||||
|
this.#eoseRelays.set(relay, unixNowMs());
|
||||||
|
} else {
|
||||||
|
const subQ = this.subQueries.find(a => a.id === sub);
|
||||||
|
if (subQ) {
|
||||||
|
subQ.eose(sub, relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the progress to EOSE, can be used to determine when we should load more content
|
||||||
|
*/
|
||||||
|
get progress() {
|
||||||
|
const thisProgress = this.#eoseRelays.size / this.#sentToRelays.reduce((acc, v) => (acc += v.Down ? 0 : 1), 0);
|
||||||
|
if (this.subQueries.length === 0) {
|
||||||
|
return thisProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalProgress = thisProgress;
|
||||||
|
for (const sq of this.subQueries) {
|
||||||
|
totalProgress += sq.progress;
|
||||||
|
}
|
||||||
|
return totalProgress / (this.subQueries.length + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,11 +112,11 @@ export class NostrSystem {
|
|||||||
OnEndOfStoredEvents(c: Connection, sub: string) {
|
OnEndOfStoredEvents(c: Connection, sub: string) {
|
||||||
const q = this.GetQuery(sub);
|
const q = this.GetQuery(sub);
|
||||||
if (q) {
|
if (q) {
|
||||||
q.request.finished = unixNowMs();
|
q.eose(sub, c.Address);
|
||||||
const f = this.Feeds.get(sub);
|
const f = this.Feeds.get(q.id);
|
||||||
if (f) {
|
if (f) {
|
||||||
f.eose(c.Address);
|
f.loading = q.progress <= 0.5;
|
||||||
f.loading = false;
|
console.debug(`${sub} loading=${f.loading}, progress=${q.progress}`);
|
||||||
}
|
}
|
||||||
if (!q.leaveOpen) {
|
if (!q.leaveOpen) {
|
||||||
c.CloseReq(sub);
|
c.CloseReq(sub);
|
||||||
@ -255,8 +255,6 @@ export class NostrSystem {
|
|||||||
this.Queries.set(rb.id, q);
|
this.Queries.set(rb.id, q);
|
||||||
const store = new type();
|
const store = new type();
|
||||||
this.Feeds.set(rb.id, store);
|
this.Feeds.set(rb.id, store);
|
||||||
store.onEose(c => console.debug(`[EOSE][${rb.id}]: ${c}`));
|
|
||||||
|
|
||||||
this.SendQuery(q);
|
this.SendQuery(q);
|
||||||
this.#changed();
|
this.#changed();
|
||||||
return store;
|
return store;
|
||||||
|
@ -60,6 +60,7 @@ export class Connection {
|
|||||||
Authed: boolean;
|
Authed: boolean;
|
||||||
Ephemeral: boolean;
|
Ephemeral: boolean;
|
||||||
EphemeralTimeout: ReturnType<typeof setTimeout> | undefined;
|
EphemeralTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
Down = true;
|
||||||
|
|
||||||
constructor(addr: string, options: RelaySettings, auth?: AuthHandler, ephemeral: boolean = false) {
|
constructor(addr: string, options: RelaySettings, auth?: AuthHandler, ephemeral: boolean = false) {
|
||||||
this.Id = uuid();
|
this.Id = uuid();
|
||||||
@ -143,6 +144,7 @@ export class Connection {
|
|||||||
OnOpen() {
|
OnOpen() {
|
||||||
this.ConnectTimeout = DefaultConnectTimeout;
|
this.ConnectTimeout = DefaultConnectTimeout;
|
||||||
console.log(`[${this.Address}] Open!`);
|
console.log(`[${this.Address}] Open!`);
|
||||||
|
this.Down = false;
|
||||||
this.OnConnected?.();
|
this.OnConnected?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +302,7 @@ export class Connection {
|
|||||||
CloseReq(id: string) {
|
CloseReq(id: string) {
|
||||||
if (this.ActiveRequests.delete(id)) {
|
if (this.ActiveRequests.delete(id)) {
|
||||||
this.#SendJson(["CLOSE", id]);
|
this.#SendJson(["CLOSE", id]);
|
||||||
|
this.OnEose?.(id);
|
||||||
this.#SendQueuedRequests();
|
this.#SendQueuedRequests();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -319,6 +322,10 @@ export class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ResetQueues() {
|
#ResetQueues() {
|
||||||
|
//send EOSE on disconnect for active subs
|
||||||
|
this.ActiveRequests.forEach(v => this.OnEose?.(v))
|
||||||
|
this.PendingRequests.forEach(v => this.OnEose?.(v[1]));
|
||||||
|
|
||||||
this.ActiveRequests.clear();
|
this.ActiveRequests.clear();
|
||||||
this.PendingRequests = [];
|
this.PendingRequests = [];
|
||||||
this.PendingRaw = [];
|
this.PendingRaw = [];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user