/** @jsx h */ import { h, render, VNode } from "https://esm.sh/preact@10.11.3"; import * as dm from "../features/dm.ts"; import { DirectMessageContainer, MessageThread } from "./dm.tsx"; import * as db from "../database.ts"; import { tw } from "https://esm.sh/twind@0.16.16"; import { EditProfile } from "./edit-profile.tsx"; import * as nav from "./nav.tsx"; import { EventBus } from "../event-bus.ts"; import { MessagePanel } from "./message-panel.tsx"; import { Setting } from "./setting.tsx"; import { Database_Contextual_View } from "../database.ts"; import { getAllUsersInformation, ProfilesSyncer, UserInfo } from "./contact-list.ts"; import { new_DM_EditorModel } from "./editor.tsx"; import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { initialModel, Model } from "./app_model.ts"; import { AppEventBus, Database_Update, Relay_Update, UI_Interaction_Event, UI_Interaction_Update, } from "./app_update.ts"; import { getSocialPosts } from "../features/social.ts"; import * as time from "../time.ts"; import { PublicKey } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts"; import { DecryptionFailure, decryptNostrEvent, NostrAccountContext, NostrEvent, NostrKind, } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; import { ConnectionPool, newSubID, } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/relay.ts"; import { getCurrentSignInCtx, setSignInState, SignIn } from "./signIn.tsx"; import { AppList } from "./app-list.tsx"; import { SecondaryBackgroundColor } from "./style/colors.ts"; import { EventSyncer } from "./event_syncer.ts"; import { getRelayURLs } from "./setting.ts"; import { DexieDatabase } from "./dexie-db.ts"; export async function Start(database: DexieDatabase) { const model = initialModel(); const eventBus = new EventBus(); const pool = new ConnectionPool(); console.log("Start the application"); const ctx = await getCurrentSignInCtx(); console.log("Start:", ctx); if (ctx instanceof Error) { console.error(ctx); model.signIn.warningString = "Please add your private key to your NIP-7 extension"; } else if (ctx) { const dbView = await Database_Contextual_View.New(database, ctx); const lamport = time.fromEvents(dbView.filterEvents((_) => true)); const app = new App(dbView, lamport, model, ctx, eventBus, pool); const err = await app.initApp(ctx); if (err instanceof Error) { throw err; } model.app = app; } /* first render */ render(, document.body); for await (let _ of UI_Interaction_Update(model, eventBus, database, pool)) { const t = Date.now(); { render(, document.body); } console.log("render", Date.now() - t); } (async () => { for await (let _ of Relay_Update(pool)) { render(, document.body); } })(); } async function initProfileSyncer( pool: ConnectionPool, accountContext: NostrAccountContext, database: db.Database_Contextual_View, ) { const myPublicKey = accountContext.publicKey; //////////////////// // Init Core Data // //////////////////// const newestEvent = dm.getNewestEventOf(database, myPublicKey); console.info("newestEvent", newestEvent); const _24h = 60 * 60 * 24; let since: number = _24h; if (newestEvent !== db.NotFound) { since = newestEvent.created_at - _24h; } console.info("since", new Date(since * 1000)); // Sync DM events const messageStream = dm.getAllEncryptedMessagesOf( myPublicKey, pool, since, ); database.syncNewDirectMessageEventsOf( accountContext, messageStream, ); // Sync my profile events const profilesSyncer = new ProfilesSyncer(database, pool); await profilesSyncer.add(myPublicKey.hex); // Sync Custom App Data (async () => { const chan = new Channel<[NostrEvent, string]>(); let subId = newSubID(); let resp = await pool.newSub( subId, { authors: [myPublicKey.hex], kinds: [NostrKind.CustomAppData], }, ); if (resp instanceof Error) { throw resp; } (async () => { for await (let { res: nostrMessage, url: relayUrl } of resp) { if (nostrMessage.type === "EVENT" && nostrMessage.event.content) { const event = nostrMessage.event; const decryptedEvent = await decryptNostrEvent( event, accountContext, accountContext.publicKey.hex, ); if (decryptedEvent instanceof DecryptionFailure) { console.error(decryptedEvent); continue; } await chan.put([ decryptedEvent, relayUrl, ]); } } console.log("closed"); })(); return chan; })().then((customAppDataChan) => { database.syncEvents((e) => e.kind == NostrKind.CustomAppData, customAppDataChan); }); /////////////////////////////////// // Add relays to Connection Pool // /////////////////////////////////// const relayURLs = getRelayURLs(database); pool.addRelayURLs(relayURLs); return profilesSyncer; } export class App { profileSyncer!: ProfilesSyncer; eventSyncer: EventSyncer; constructor( public readonly database: Database_Contextual_View, public readonly lamport: time.LamportTime, public readonly model: Model, public readonly myAccountContext: NostrAccountContext, public readonly eventBus: EventBus, public readonly relayPool: ConnectionPool, ) { this.eventSyncer = new EventSyncer(this.relayPool, this.database); } initApp = async (accountContext: NostrAccountContext) => { console.log("App.initApp"); const profilesSyncer = await initProfileSyncer(this.relayPool, accountContext, this.database); if (profilesSyncer instanceof Error) { return profilesSyncer; } this.profileSyncer = profilesSyncer; const allUsersInfo = getAllUsersInformation(this.database, this.myAccountContext); console.log("App allUsersInfo"); /* my profile */ this.model.myProfile = allUsersInfo.get(accountContext.publicKey.hex)?.profile?.content; /* contacts */ for (const contact of allUsersInfo.values()) { const editor = this.model.editors.get(contact.pubkey.hex); if (editor == null) { const pubkey = PublicKey.FromHex(contact.pubkey.hex); if (pubkey instanceof Error) { throw pubkey; // impossible } this.model.editors.set( contact.pubkey.hex, new_DM_EditorModel({ pubkey, name: contact.profile?.content.name, picture: contact.profile?.content.picture, }), ); } } await profilesSyncer.add( ...Array.from(allUsersInfo.keys()), ); console.log("user set", profilesSyncer.userSet); // Database (async () => { let i = 0; for await ( let _ of Database_Update( accountContext, this.database, this.model, this.profileSyncer, this.lamport, this.eventBus, ) ) { render(, document.body); console.log(`render ${++i} times`); } })(); }; logout = () => { setSignInState("none"); window.location.reload(); }; } export function AppComponent(props: { model: Model; eventBus: AppEventBus; }) { 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.myAccountContext; let socialPostsPanel: VNode | undefined; if (model.navigationModel.activeNav == "Social") { const allUserInfo = getAllUsersInformation(app.database, myAccountCtx); console.log("AppComponent:getSocialPosts before", Date.now() - t); const socialPosts = getSocialPosts(app.database, allUserInfo); console.log("AppComponent:getSocialPosts after", Date.now() - t, Date.now()); let focusedContentGetter = () => { console.log("AppComponent:getFocusedContent before", Date.now() - t); let _ = getFocusedContent(model.social.focusedContent, allUserInfo, socialPosts); console.log("AppComponent:getFocusedContent", Date.now() - t); if (_?.type === "MessageThread") { let editor = model.social.replyEditors.get(_.data.root.event.id); if (editor == undefined) { editor = { id: _.data.root.event.id, files: [], text: "", tags: [ ["e", _.data.root.event.id], ], target: { kind: NostrKind.TEXT_NOTE, }, }; model.social.replyEditors.set(editor.id, editor); } return { ..._, editor, }; } return _; }; console.log("AppComponent:focusedContentGetter", Date.now() - t); let focusedContent = focusedContentGetter(); console.log("AppComponent:socialPosts", Date.now() - t); socialPostsPanel = (
); } let settingNode; if (model.navigationModel.activeNav == "Setting") { settingNode = (
{Setting({ logout: app.logout, pool: app.relayPool, eventBus: app.eventBus, AddRelayButtonClickedError: model.AddRelayButtonClickedError, AddRelayInput: model.AddRelayInput, myAccountContext: myAccountCtx, })}
); } let appList; if (model.navigationModel.activeNav == "AppList") { appList = (
); } let dmVNode; let aboutNode; if ( model.navigationModel.activeNav == "DM" || model.navigationModel.activeNav == "About" ) { const allUserInfo = getAllUsersInformation(app.database, myAccountCtx); if (model.navigationModel.activeNav == "DM") { dmVNode = (
{DirectMessageContainer({ editors: model.editors, ...model.dm, rightPanelModel: model.rightPanelModel, eventEmitter: app.eventBus, myAccountContext: myAccountCtx, db: app.database, pool: app.relayPool, allUserInfo: allUserInfo, profilesSyncer: app.profileSyncer, eventSyncer: app.eventSyncer, })}
); } if (model.navigationModel.activeNav == "About") { aboutNode = About(); } } const final = (
{nav.NavBar({ profilePicURL: model.myProfile?.picture, publicKey: myAccountCtx.publicKey, database: app.database, pool: app.relayPool, eventEmitter: app.eventBus, AddRelayButtonClickedError: model.AddRelayButtonClickedError, AddRelayInput: model.AddRelayInput, ...model.navigationModel, })}
{EditProfile({ eventEmitter: app.eventBus, myProfile: model.myProfile, newProfileField: model.newProfileField, })}
{dmVNode} {aboutNode} {settingNode} {socialPostsPanel} {appList}
); console.debug("App:end", Date.now() - t); return final; } function About() { return (

Blowater is delightful DM focusing Nostr client.

It's here to replace Telegram/Slack/Discord alike centralized chat apps and give users a strong privacy, globally available decentralized chat app.

Authors

  • Water Blowater npub1dww6jgxykmkt7tqjqx985tg58dxlm7v83sa743578xa4j7zpe3hql6pdnf
Donation Lightning: blowater@getalby.com
Customer Support Support Bot: npub1fdjk8cz47lzmcruean82cfufefkf4gja9hrs90tyysemm5p7vt7s9knc27
); } // todo: move to somewhere else export function getFocusedContent( focusedContent: PublicKey | NostrEvent | undefined, allUserInfo: Map, threads: MessageThread[], ) { if (focusedContent == undefined) { return; } if (focusedContent instanceof PublicKey) { const profileData = allUserInfo.get(focusedContent.hex)?.profile?.content; return { type: "ProfileData" as "ProfileData", data: profileData, pubkey: focusedContent, }; } else { for (const thread of threads) { if (thread.root.event.id == focusedContent.id) { return { type: "MessageThread" as "MessageThread", data: thread, }; } } } }