/** @jsx h */ import { h, render, VNode } from "https://esm.sh/preact@10.17.1"; import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { InvalidKey, PublicKey } from "../../libs/nostr.ts/key.ts"; import { NostrAccountContext, NostrEvent, NostrKind } from "../../libs/nostr.ts/nostr.ts"; import { ConnectionPool } from "../../libs/nostr.ts/relay-pool.ts"; import { Database_View } from "../database.ts"; import { EventBus } from "../event-bus.ts"; import { DirectedMessageController, getAllEncryptedMessagesOf, InvalidEvent } from "../features/dm.ts"; import { About } from "./about.tsx"; import { initialModel, Model } from "./app_model.ts"; import { AppEventBus, Database_Update, UI_Interaction_Event, UI_Interaction_Update } from "./app_update.tsx"; import { Popover, PopOverInputChannel } from "./components/popover.tsx"; import { OtherConfig } from "./config-other.ts"; import { DM_List } from "./conversation-list.ts"; import { DexieDatabase } from "./dexie-db.ts"; import { DirectMessageContainer } from "./dm.tsx"; import { EditProfile } from "./edit-profile.tsx"; import { RelayConfig } from "./relay-config.ts"; import { ProfileGetter } from "./search.tsx"; import { Setting } from "./setting.tsx"; import { getCurrentSignInCtx, getSignInState, setSignInState } from "./sign-in.ts"; import { SecondaryBackgroundColor } from "./style/colors.ts"; import { LamportTime } from "../time.ts"; import { InstallPrompt, NavBar } from "./nav.tsx"; import { Component } from "https://esm.sh/preact@10.17.1"; import { SingleRelayConnection } from "../../libs/nostr.ts/relay-single.ts"; import { PublicMessageContainer } from "./public-message-container.tsx"; import { ChatMessage } from "./message.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"; import { getTags, Parsed_Event } from "../nostr.ts"; import { Toast } from "./components/toast.tsx"; import { ToastChannel } from "./components/toast.tsx"; import { RightPanelChannel } from "./components/right-panel.tsx"; export async function Start(database: DexieDatabase) { console.log("Start the application"); const installPrompt: InstallPrompt = { event: undefined, }; window.addEventListener("beforeinstallprompt", async (event) => { event.preventDefault(); installPrompt.event = event; }); const lamport = new LamportTime(); const model = initialModel(); const eventBus = new EventBus(); const pool = new ConnectionPool(); const popOverInputChan: PopOverInputChannel = new Channel(); const rightPanelInputChan: RightPanelChannel = new Channel(); const toastInputChan: ToastChannel = new Channel(); const dbView = await Database_View.New(database, database, database); const newNostrEventChannel = new Channel(); (async () => { for await (const event of newNostrEventChannel) { const err = await pool.sendEvent(event); if (err instanceof Error) { console.error(err); } } })(); { for (;;) { if (getSignInState() === "none") { break; } const ctx = await getCurrentSignInCtx(); if (ctx instanceof Error) { console.error(ctx); break; } else if (ctx) { const otherConfig = await OtherConfig.FromLocalStorage(ctx, newNostrEventChannel, lamport); const app = await App.Start({ database: dbView, model, ctx, eventBus, pool, popOverInputChan, rightPanelInputChan, otherConfig, lamport, installPrompt, toastInputChan, }); model.app = app; break; } } } /* first render */ render( , document.body, ); for await ( let _ of UI_Interaction_Update({ model, eventBus, dbView: dbView, pool, popOver: popOverInputChan, rightPanel: rightPanelInputChan, newNostrEventChannel: newNostrEventChannel, lamport, installPrompt, toastInputChan: toastInputChan, }) ) { const t = Date.now(); { render( , document.body, ); } console.log("UI_Interaction_Update render:", Date.now() - t); } } export class App { private constructor( public readonly database: Database_View, public readonly model: Model, public readonly ctx: NostrAccountContext, public readonly eventBus: EventBus, public readonly pool: ConnectionPool, public readonly popOverInputChan: PopOverInputChannel, public readonly rightPanelInputChan: RightPanelChannel, public readonly otherConfig: OtherConfig, public readonly conversationLists: DM_List, public readonly relayConfig: RelayConfig, public readonly lamport: LamportTime, public readonly dmController: DirectedMessageController, public readonly toastInputChan: ToastChannel, ) {} static async Start(args: { database: Database_View; model: Model; ctx: NostrAccountContext; eventBus: EventBus; pool: ConnectionPool; popOverInputChan: PopOverInputChannel; rightPanelInputChan: RightPanelChannel; otherConfig: OtherConfig; lamport: LamportTime; installPrompt: InstallPrompt; toastInputChan: ToastChannel; }) { const all_events = Array.from(args.database.getAllEvents()); args.lamport.fromEvents(all_events); // init relay config const relayConfig = await RelayConfig.FromLocalStorage({ ctx: args.ctx, relayPool: args.pool, }); console.log(relayConfig.getRelayURLs()); // init conversation list const conversationLists = new DM_List(args.ctx); const err = conversationLists.addEvents(all_events, false); if (err instanceof InvalidEvent) { console.error(err); await args.database.remove(err.event.id); } const dmController = new DirectedMessageController(args.ctx); (async () => { // load DMs for (const e of all_events) { if (e.kind == NostrKind.DIRECT_MESSAGE) { const error = await dmController.addEvent({ ...e, kind: e.kind, }); if (error instanceof Error) { console.error(error.message); if (error instanceof InvalidKey) { await args.database.remove(e.id); } } } else { continue; } } })(); const app = new App( args.database, args.model, args.ctx, args.eventBus, args.pool, args.popOverInputChan, args.rightPanelInputChan, args.otherConfig, conversationLists, relayConfig, args.lamport, dmController, args.toastInputChan, ); await app.initApp(args.installPrompt); return app; } private initApp = async (installPrompt: InstallPrompt) => { console.log("App.initApp"); // Sync events { forever(sync_client_specific_data(this.pool, this.ctx, this.database)); forever(sync_dm_events(this.database, this.ctx, this.pool)); forever(sync_profile_events(this.database, this.pool)); forever(sync_kind_1(this.pool, this.database)); } // Database (async () => { let i = 0; for await ( let _ of Database_Update( this.ctx, this.database, this.model, this.lamport, this.conversationLists, this.dmController, this.eventBus.emit, { otherConfig: this.otherConfig, }, ) ) { const t = Date.now(); render( , document.body, ); console.log(`Database_Update: render ${++i} times, ${Date.now() - t}`); } })(); }; logout = () => { setSignInState("none"); window.location.reload(); }; } type AppProps = { model: Model; eventBus: AppEventBus; pool: ConnectionPool; popOverInputChan: PopOverInputChannel; rightPanelInputChan: RightPanelChannel; toastInputChan: ToastChannel; installPrompt: InstallPrompt; }; export class AppComponent extends Component { events = this.props.eventBus.onChange(); componentWillUnmount() { this.events.close(); } render(props: AppProps) { const t = Date.now(); const model = props.model; if (model.app == undefined) { console.log("render sign in page"); return ; } const app = model.app; const myAccountCtx = model.app.ctx; let dmVNode; let aboutNode; if ( model.navigationModel.activeNav == "DM" || model.navigationModel.activeNav == "About" ) { if (model.navigationModel.activeNav == "DM" && model.currentRelay) { dmVNode = ( ); } if (model.navigationModel.activeNav == "About") { aboutNode = About(app.eventBus.emit); } } let publicNode: VNode | undefined; if (model.navigationModel.activeNav == "Public" && model.currentRelay) { publicNode = ( { if (e.kind != NostrKind.TEXT_NOTE) { return false; } const relays = app.database.getRelayRecord(e.id); return relays.has(model.currentRelay); }, ), (e) => { const msg: ChatMessage = { author: e.publicKey, content: e.content, created_at: new Date(e.created_at * 1000), event: e as Parsed_Event, lamport: getTags(e).lamport_timestamp, type: "text", }; return msg; }, ), )} relay={props.pool.getRelay(model.currentRelay) as SingleRelayConnection} bus={app.eventBus} /> ); } const final = (
{publicNode} {dmVNode} {aboutNode} {Setting({ show: model.navigationModel.activeNav == "Setting", logout: app.logout, relayConfig: app.relayConfig, myAccountContext: myAccountCtx, relayPool: props.pool, emit: props.eventBus.emit, })}
); console.debug("AppComponent:end", Date.now() - t); return final; } } // todo: move to somewhere else export function getFocusedContent( focusedContent: PublicKey | NostrEvent | undefined, profileGetter: ProfileGetter, ) { if (focusedContent == undefined) { return; } if (focusedContent instanceof PublicKey) { const profileData = profileGetter.getProfilesByPublicKey(focusedContent)?.profile; return { type: "ProfileData" as "ProfileData", data: profileData, pubkey: focusedContent, }; } } async function sync_dm_events( database: Database_View, ctx: NostrAccountContext, pool: ConnectionPool, ) { const messageStream = getAllEncryptedMessagesOf( ctx.publicKey, pool, ); for await (const msg of messageStream) { if (msg.res.type == "EVENT") { const err = await database.addEvent(msg.res.event, msg.url); if (err instanceof Error) { console.log(err); } } } } async function sync_profile_events( database: Database_View, pool: ConnectionPool, ) { const messageStream = await pool.newSub("sync_profile_events", { kinds: [NostrKind.META_DATA], }); if (messageStream instanceof Error) { return messageStream; } for await (const msg of messageStream.chan) { if (msg.res.type == "EVENT") { const err = await database.addEvent(msg.res.event, msg.url); if (err instanceof Error) { console.log(err); } } } } const sync_kind_1 = async (pool: ConnectionPool, database: Database_View) => { const stream = await pool.newSub("sync_kind_1", { kinds: [NostrKind.TEXT_NOTE], since: Math.floor(Date.now() / 1000) - 6 * 60 * 60, // 6 hours }); if (stream instanceof Error) { return stream; } for await (const msg of stream.chan) { if (msg.res.type == "EOSE" || msg.res.type == "NOTICE") { continue; } const ok = await database.addEvent(msg.res.event, msg.url); if (ok instanceof Error) { console.error(msg); console.error(ok); } } }; const sync_client_specific_data = async ( pool: ConnectionPool, ctx: NostrAccountContext, database: Database_View, ) => { const stream = await pool.newSub(OtherConfig.name, { authors: [ctx.publicKey.hex], kinds: [NostrKind.Encrypted_Custom_App_Data], }); if (stream instanceof Error) { throw stream; // crash the app } for await (const msg of stream.chan) { if (msg.res.type == "EOSE" || msg.res.type == "NOTICE") { continue; } const ok = await database.addEvent(msg.res.event, msg.url); if (ok instanceof Error) { console.error(msg.res.event); console.error(ok); } } };