more responsive rendering on new event from relays & auto scroll to posted message on public feed (#418)

This commit is contained in:
BlowaterNostr 2024-03-17 17:00:07 +08:00 committed by GitHub
parent 8fd31d1663
commit 9d2901fe39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 113 additions and 47 deletions

View File

@ -25,3 +25,12 @@ export function* filter<X>(iter: Iterable<X>, filterer: (item: X) => boolean) {
}
}
}
// f should not resolve, if it does resolve, it should only throw an error
export async function forever(f: Promise<Error | undefined | void>) {
const r = await f;
if (r == undefined) {
throw new Error(`${f} should not resolve`);
}
throw r;
}

View File

@ -27,7 +27,7 @@ import { Component } from "https://esm.sh/preact@10.17.1";
import { SingleRelayConnection } from "../../libs/nostr.ts/relay-single.ts";
import { ChannelContainer } from "./channel-container.tsx";
import { ChatMessage } from "./message.ts";
import { filter, map } from "./_helper.ts";
import { filter, forever, map } from "./_helper.ts";
import { RightPanel } from "./components/right-panel.tsx";
import { ComponentChildren } from "https://esm.sh/preact@10.17.1";
import { SignIn } from "./sign-in.tsx";
@ -108,7 +108,7 @@ export async function Start(database: DexieDatabase) {
);
for await (
let ok of UI_Interaction_Update({
let _ of UI_Interaction_Update({
model,
eventBus,
dbView: dbView,
@ -121,9 +121,6 @@ export async function Start(database: DexieDatabase) {
toastInputChan: toastInputChan,
})
) {
if (ok == false) {
continue;
}
const t = Date.now();
{
render(
@ -396,8 +393,6 @@ export class AppComponent extends Component<AppProps> {
);
}
console.debug("AppComponent:2", Date.now() - t);
const final = (
<div class={`h-screen w-full flex`}>
<NavBar
@ -551,12 +546,3 @@ const sync_client_specific_data = async (
}
}
};
// f should not resolve, if it does resolve, it should only throw an error
async function forever(f: Promise<Error | undefined | void>) {
const r = await f;
if (r == undefined) {
throw new Error(`${f} should not resolve`);
}
throw r;
}

View File

@ -1,6 +1,12 @@
/** @jsx h */
import { ComponentChildren, h } from "https://esm.sh/preact@10.17.1";
import { Channel, closed, sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import {
Channel,
closed,
PopChannel,
PutChannel,
sleep,
} from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
@ -49,6 +55,7 @@ import { SyncEvent } from "./message-panel.tsx";
import { SendingEventRejection, ToastChannel } from "./components/toast.tsx";
import { SingleRelayConnection } from "../../libs/nostr.ts/relay-single.ts";
import { default_blowater_relay } from "./relay-config.ts";
import { forever } from "./_helper.ts";
export type UI_Interaction_Event =
| SearchUpdate
@ -92,7 +99,7 @@ export type UserBlocker = {
/////////////////////
// UI Interfaction //
/////////////////////
export async function* UI_Interaction_Update(args: {
export function UI_Interaction_Update(args: {
model: Model;
eventBus: AppEventBus;
dbView: Database_View;
@ -103,7 +110,24 @@ export async function* UI_Interaction_Update(args: {
lamport: LamportTime;
installPrompt: InstallPrompt;
toastInputChan: ToastChannel;
}) {
}): Channel<true> {
const chan = new Channel<true>();
forever(handle_update_event(chan, args));
return chan;
}
const handle_update_event = async (chan: PutChannel<true>, args: {
model: Model;
eventBus: AppEventBus;
dbView: Database_View;
pool: ConnectionPool;
popOver: PopOverInputChannel;
rightPanel: Channel<() => ComponentChildren>;
newNostrEventChannel: Channel<NostrEvent>;
lamport: LamportTime;
installPrompt: InstallPrompt;
toastInputChan: ToastChannel;
}) => {
const { model, dbView, eventBus, pool, installPrompt } = args;
for await (const event of eventBus.onChange()) {
console.log(event);
@ -133,7 +157,6 @@ export async function* UI_Interaction_Update(args: {
} else {
console.error("failed to sign in");
}
yield model;
continue;
}
@ -244,6 +267,8 @@ export async function* UI_Interaction_Update(args: {
app.toastInputChan.put(
SendingEventRejection(eventBus.emit, current_relay.url, res.message),
);
} else {
chan.put(true);
}
});
} else if (event.type == "UpdateMessageFiles") {
@ -298,7 +323,8 @@ export async function* UI_Interaction_Update(args: {
() => {
return (
<UserDetail
targetUserProfile={app.database.getProfilesByPublicKey(event.pubkey)?.profile ||
targetUserProfile={app.database.getProfilesByPublicKey(event.pubkey)
?.profile ||
{}}
pubkey={event.pubkey}
emit={eventBus.emit}
@ -426,16 +452,14 @@ export async function* UI_Interaction_Update(args: {
}
});
}
yield false; // do not update UI
continue;
} else {
console.log(event, "is not handled");
yield false;
continue;
}
yield true;
await chan.put(true);
}
}
};
export type DirectMessageGetter = ChatMessagesGetter & {
getDirectMessageStream(publicKey: string): Channel<ChatMessage>;
@ -474,7 +498,7 @@ export async function* Database_Update(
console.error("unreachable: db changes channel should never close");
break;
}
changes_events.push(e);
changes_events.push(e.event);
}
convoLists.addEvents(changes_events, true);

View File

@ -51,6 +51,18 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
jitter = new JitterPrevention(100);
async componentDidUpdate(previousProps: Readonly<MessageListProps>) {
const newest = last(this.props.messages);
const pre_newest = last(previousProps.messages);
if (
newest && pre_newest && newest.author.hex == this.props.myPublicKey.hex &&
newest.event.id != pre_newest.event.id
) {
await this.goToLastPage();
this.goToButtom(false);
}
}
async componentDidMount() {
const offset = this.props.messages.length - ItemsOfPerPage;
await setState(this, { offset: offset <= 0 ? 0 : offset });
@ -58,7 +70,6 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
render() {
const messages_to_render = this.sortAndSliceMessage();
console.log(messages_to_render);
const groups = groupContinuousMessages(messages_to_render, (pre, cur) => {
const sameAuthor = pre.event.pubkey == cur.event.pubkey;
const _66sec = Math.abs(cur.created_at.getTime() - pre.created_at.getTime()) <
@ -88,7 +99,7 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
}}
>
<button
onClick={this.goToButtom}
onClick={() => this.goToButtom(true)}
class={`${IconButtonClass} fixed z-10 bottom-8 right-4 h-10 w-10 rotate-[-90deg] bg-[#42464D] hover:bg-[#2F3136]`}
>
<LeftArrowIcon
@ -136,15 +147,23 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
}
};
goToButtom = () => {
goToButtom = (smooth: boolean) => {
if (this.messagesULElement.current) {
this.messagesULElement.current.scrollTo({
top: this.messagesULElement.current.scrollHeight,
left: 0,
behavior: "smooth",
behavior: smooth ? "smooth" : undefined,
});
}
};
goToLastPage = async () => {
const newOffset = this.props.messages.length - ItemsOfPerPage / 2;
await setState(this, {
offset: newOffset > 0 ? newOffset : 0,
});
console.log("goToLastPage", this.state.offset);
};
}
export class MessageList_V0 extends Component<MessageListProps> {
@ -379,3 +398,11 @@ function MessageActions(
</div>
);
}
function last<T>(array: Array<T>): T | undefined {
if (array.length == 0) {
return undefined;
} else {
return array[array.length - 1];
}
}

View File

@ -1,9 +1,11 @@
import { not_cancelled, sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { prepareNormalNostrEvent } from "../libs/nostr.ts/event.ts";
import { PrivateKey } from "../libs/nostr.ts/key.ts";
import { InMemoryAccountContext, NostrEvent, NostrKind } from "../libs/nostr.ts/nostr.ts";
import { assertEquals, fail } from "https://deno.land/std@0.202.0/testing/asserts.ts";
import { InMemoryAccountContext, NostrKind } from "../libs/nostr.ts/nostr.ts";
import { test_db_view } from "./UI/_setup.test.ts";
import { Parsed_Event } from "./nostr.ts";
import { assertEquals } from "https://deno.land/std@0.202.0/assert/assert_equals.ts";
import { fail } from "https://deno.land/std@0.202.0/assert/fail.ts";
Deno.test("Database", async () => {
const ctx = InMemoryAccountContext.New(PrivateKey.Generate());
@ -29,7 +31,11 @@ Deno.test("Database", async () => {
event_to_add,
);
const e = await stream.pop() as NostrEvent;
const res = await stream.pop() as {
event: Parsed_Event;
relay?: string | undefined;
};
const e = res.event;
assertEquals(
{
content: e.content,
@ -49,8 +55,18 @@ Deno.test("Database", async () => {
const event_to_add2 = await prepareNormalNostrEvent(ctx, { kind: NostrKind.TEXT_NOTE, content: "2" });
// console.log(event_to_add2.id, event_to_add.id)
await db.addEvent(event_to_add2);
const e2 = await stream.pop() as NostrEvent;
assertEquals(e2, await stream2.pop() as NostrEvent);
const res_2 = await stream.pop() as {
event: Parsed_Event;
relay?: string | undefined;
};
const e2 = res_2.event;
assertEquals(
res_2,
await stream2.pop() as {
event: Parsed_Event;
relay?: string | undefined;
},
);
assertEquals({
content: e2.content,
created_at: e2.created_at,
@ -70,10 +86,10 @@ Deno.test("Relay Record", async () => {
const event_to_add = await prepareNormalNostrEvent(ctx, { kind: NostrKind.TEXT_NOTE, content: "1" });
const event_to_add_2 = await prepareNormalNostrEvent(ctx, { kind: NostrKind.TEXT_NOTE, content: "2" });
await db.addEvent(event_to_add); // send by client
assertEquals(await db.getRelayRecord(event_to_add.id), new Set<string>());
assertEquals(db.getRelayRecord(event_to_add.id), new Set<string>());
await db.addEvent(event_to_add_2, "wss://relay.blowater.app"); // receiver from relay
assertEquals(await db.getRelayRecord(event_to_add_2.id), new Set(["wss://relay.blowater.app"]));
assertEquals(db.getRelayRecord(event_to_add_2.id), new Set(["wss://relay.blowater.app"]));
await db.addEvent(event_to_add_2, "wss://relay.test.app");
assertEquals(
@ -86,6 +102,7 @@ Deno.test("Relay Record", async () => {
),
);
await stream.pop();
await stream.pop();
await stream.pop();

View File

@ -64,8 +64,8 @@ export interface RelayRecordGetter {
export class Database_View
implements ProfileSetter, ProfileGetter, EventGetter, EventRemover, RelayRecordGetter {
//
public readonly sourceOfChange = csp.chan<Parsed_Event>(buffer_size);
private readonly caster = csp.multi<Parsed_Event>(this.sourceOfChange);
public readonly sourceOfChange = csp.chan<{ event: Parsed_Event; relay?: string }>(buffer_size);
private readonly caster = csp.multi<{ event: Parsed_Event; relay?: string }>(this.sourceOfChange);
private readonly profiles = new Map<string, Profile_Nostr_Event>();
private constructor(
@ -218,12 +218,6 @@ export class Database_View
await this.recordRelay(event.id, url);
}
// check if the event exists
const storedEvent = await this.eventsAdapter.get({ id: event.id });
if (storedEvent) { // event exist
return false;
}
// parse the event to desired format
const pubkey = PublicKey.FromHex(event.pubkey);
if (pubkey instanceof Error) {
@ -236,6 +230,15 @@ export class Database_View
publicKey: pubkey,
};
// check if the event exists
const storedEvent = await this.eventsAdapter.get({ id: event.id });
if (storedEvent) { // event exist
if (url) {
this.sourceOfChange.put({ event: parsedEvent, relay: url });
}
return false;
}
// add event to database and notify subscribers
console.log("Database.addEvent", event);
@ -251,7 +254,7 @@ export class Database_View
}
await this.eventsAdapter.put(event);
/* not await */ this.sourceOfChange.put(parsedEvent);
/* not await */ this.sourceOfChange.put({ event: parsedEvent, relay: url });
return parsedEvent;
}
@ -260,7 +263,7 @@ export class Database_View
//////////////////
subscribe() {
const c = this.caster.copy();
const res = csp.chan<Parsed_Event>(buffer_size);
const res = csp.chan<{ event: Parsed_Event; relay?: string }>(buffer_size);
(async () => {
for await (const newE of c) {
const err = await res.put(newE);