blowater/UI/app.tsx

491 lines
17 KiB
TypeScript
Raw Normal View History

2023-06-30 14:05:57 +00:00
/** @jsx h */
import { h, render, VNode } from "https://esm.sh/preact@10.17.1";
2023-06-30 14:05:57 +00:00
import * as dm from "../features/dm.ts";
import { DirectMessageContainer, MessageThread } from "./dm.tsx";
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 { Setting } from "./setting.tsx";
2023-07-11 09:49:58 +00:00
import { Database_Contextual_View } from "../database.ts";
2023-09-23 21:54:13 +00:00
import { ConversationLists, ConversationSummary } from "./conversation-list.ts";
2023-06-30 14:05:57 +00:00
import { new_DM_EditorModel } from "./editor.tsx";
import { initialModel, Model } from "./app_model.ts";
import { AppEventBus, Database_Update, UI_Interaction_Event, UI_Interaction_Update } from "./app_update.tsx";
2023-06-30 14:05:57 +00:00
import * as time from "../time.ts";
2023-09-28 19:37:24 +00:00
import { PublicKey } from "../lib/nostr-ts/key.ts";
2023-08-28 17:58:05 +00:00
import { NostrAccountContext, NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts";
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
2023-07-09 08:06:49 +00:00
import { getCurrentSignInCtx, setSignInState, SignIn } from "./signIn.tsx";
2023-06-30 14:05:57 +00:00
import { AppList } from "./app-list.tsx";
import { SecondaryBackgroundColor } from "./style/colors.ts";
import { EventSyncer } from "./event_syncer.ts";
import { defaultRelays, RelayConfig } from "./relay-config.ts";
2023-07-11 09:49:58 +00:00
import { DexieDatabase } from "./dexie-db.ts";
import { About } from "./about.tsx";
2023-10-01 20:59:07 +00:00
import { ProfileSyncer } from "../features/profile.ts";
import { Popover, PopOverInputChannel } from "./components/popover.tsx";
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { GroupChatController } from "../group-chat.ts";
import { OtherConfig } from "./config-other.ts";
2023-07-09 07:06:13 +00:00
2023-07-11 09:49:58 +00:00
export async function Start(database: DexieDatabase) {
console.log("Start the application");
const model = initialModel();
const eventBus = new EventBus<UI_Interaction_Event>();
const pool = new ConnectionPool();
const popOverInputChan: PopOverInputChannel = new Channel();
2023-07-09 08:06:49 +00:00
const ctx = await getCurrentSignInCtx();
2023-07-09 12:43:03 +00:00
if (ctx instanceof Error) {
console.error(ctx);
model.signIn.warningString = "Please add your private key to your NIP-7 extension";
2023-07-09 12:43:03 +00:00
} else if (ctx) {
const dbView = await Database_Contextual_View.New(database, ctx);
2023-09-12 14:51:27 +00:00
if (dbView instanceof Error) {
throw dbView;
}
const lamport = time.fromEvents(dbView.events);
const app = new App(dbView, lamport, model, ctx, eventBus, pool, popOverInputChan);
await app.initApp();
model.app = app;
2023-07-09 08:17:09 +00:00
}
2023-07-09 07:06:13 +00:00
/* first render */ render(
AppComponent({
eventBus,
model,
pool,
popOverInputChan,
}),
document.body,
);
2023-07-21 12:40:49 +00:00
for await (
let _ of UI_Interaction_Update({
model,
eventBus,
dexieDB: database,
pool,
popOver: popOverInputChan,
})
) {
2023-07-09 07:06:13 +00:00
const t = Date.now();
{
render(
AppComponent({
eventBus,
model,
pool,
popOverInputChan,
}),
document.body,
);
2023-07-09 07:06:13 +00:00
}
console.log("UI_Interaction_Update render:", Date.now() - t);
2023-07-09 07:06:13 +00:00
}
}
2023-06-30 14:05:57 +00:00
export class App {
2023-10-01 20:59:07 +00:00
readonly profileSyncer: ProfileSyncer;
readonly eventSyncer: EventSyncer;
public readonly conversationLists: ConversationLists;
public readonly relayConfig: RelayConfig;
public readonly groupChatController: GroupChatController;
public readonly otherConfig: OtherConfig = new OtherConfig();
2023-06-30 14:05:57 +00:00
2023-07-09 07:06:13 +00:00
constructor(
2023-07-11 09:49:58 +00:00
public readonly database: Database_Contextual_View,
2023-07-09 07:06:13 +00:00
public readonly lamport: time.LamportTime,
public readonly model: Model,
public readonly ctx: NostrAccountContext,
public readonly eventBus: EventBus<UI_Interaction_Event>,
public readonly pool: ConnectionPool,
public readonly popOverInputChan: PopOverInputChannel,
2023-06-30 14:05:57 +00:00
) {
this.eventSyncer = new EventSyncer(pool, this.database);
this.conversationLists = new ConversationLists(ctx);
this.conversationLists.addEvents(database.events);
this.groupChatController = new GroupChatController(ctx, this.conversationLists);
this.relayConfig = RelayConfig.FromLocalStorage(ctx);
if (this.relayConfig.getRelayURLs().size == 0) {
for (const url of defaultRelays) {
this.relayConfig.add(url);
}
}
2023-10-01 20:59:07 +00:00
this.profileSyncer = new ProfileSyncer(this.database, pool);
this.profileSyncer.add(ctx.publicKey.hex);
2023-06-30 14:05:57 +00:00
}
initApp = async () => {
2023-07-09 07:06:13 +00:00
console.log("App.initApp");
///////////////////////////////////
// Add relays to Connection Pool //
///////////////////////////////////
// relay config synchronization, need to refactor later
(async () => {
2023-09-23 21:33:30 +00:00
const err = await this.relayConfig.syncWithPool(this.pool);
if (err instanceof Error) {
throw err; // don't know what to do, should crash the app
}
const stream = await this.pool.newSub("relay config", {
"#d": ["RelayConfig"],
authors: [this.ctx.publicKey.hex],
kinds: [NostrKind.Custom_App_Data],
});
if (stream instanceof Error) {
throw stream; // impossible
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
console.log(msg.res);
RelayConfig.FromNostrEvent(msg.res.event, this.ctx);
const _relayConfig = await RelayConfig.FromNostrEvent(
msg.res.event,
this.ctx,
);
if (_relayConfig instanceof Error) {
console.log(_relayConfig.message);
continue;
}
this.relayConfig.merge(_relayConfig.save());
this.relayConfig.saveToLocalStorage(this.ctx);
}
})();
this.otherConfig.syncFromRelay(this.pool, this.ctx);
// create group synchronization
(async () => {
const stream = await this.pool.newSub("group creations", {
"#d": [GroupChatController.name],
authors: [this.ctx.publicKey.hex],
kinds: [NostrKind.Custom_App_Data],
});
if (stream instanceof Error) {
throw stream; // crash to app
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
this.groupChatController.addEvent(msg.res.event);
}
})();
// Sync DM events
(async function sync_dm_events(
database: Database_Contextual_View,
ctx: NostrAccountContext,
pool: ConnectionPool,
) {
const messageStream = dm.getAllEncryptedMessagesOf(
ctx.publicKey,
pool,
);
for await (const msg of messageStream) {
if (msg.res.type == "EVENT") {
const err = await database.addEvent(msg.res.event);
if (err instanceof Error) {
console.log(err);
}
}
}
})(this.database, this.ctx, this.pool);
2023-06-30 14:05:57 +00:00
/* my profile */
this.model.myProfile = this.conversationLists.convoSummaries.get(this.ctx.publicKey.hex)?.profile
?.profile;
2023-06-30 14:05:57 +00:00
/* contacts */
for (const contact of this.conversationLists.convoSummaries.values()) {
2023-06-30 14:05:57 +00:00
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,
2023-07-14 10:59:25 +00:00
name: contact.profile?.profile.name,
picture: contact.profile?.profile.picture,
2023-06-30 14:05:57 +00:00
}),
);
}
}
this.profileSyncer.add(
...Array.from(this.conversationLists.convoSummaries.keys()),
2023-06-30 14:05:57 +00:00
);
console.log("user set", this.profileSyncer.userSet);
2023-06-30 14:05:57 +00:00
const ps = Array.from(this.conversationLists.convoSummaries.values()).map((u) => u.pubkey.hex);
2023-08-03 09:13:16 +00:00
this.eventSyncer.syncEvents({
kinds: [NostrKind.TEXT_NOTE],
authors: ps,
});
2023-07-09 07:06:13 +00:00
// Database
2023-06-30 14:05:57 +00:00
(async () => {
2023-07-09 07:06:13 +00:00
let i = 0;
2023-06-30 14:05:57 +00:00
for await (
let _ of Database_Update(
this.ctx,
2023-06-30 14:05:57 +00:00
this.database,
this.model,
2023-07-09 07:06:13 +00:00
this.profileSyncer,
this.lamport,
this.conversationLists,
this.eventBus.emit,
2023-06-30 14:05:57 +00:00
)
) {
const t = Date.now();
render(
AppComponent({
eventBus: this.eventBus,
model: this.model,
pool: this.pool,
popOverInputChan: this.popOverInputChan,
}),
document.body,
);
console.log(`Database_Update: render ${++i} times, ${Date.now() - t}`);
2023-06-30 14:05:57 +00:00
}
})();
};
logout = () => {
setSignInState("none");
window.location.reload();
};
}
export function AppComponent(props: {
model: Model;
eventBus: AppEventBus;
pool: ConnectionPool;
popOverInputChan: PopOverInputChannel;
2023-06-30 14:05:57 +00:00
}) {
const t = Date.now();
const model = props.model;
if (model.app == undefined) {
2023-06-30 14:05:57 +00:00
console.log("render sign in page");
return (
<SignIn
eventBus={props.eventBus}
privateKey={model.signIn.privateKey}
warningString={model.signIn.warningString}
2023-06-30 14:05:57 +00:00
/>
);
}
const app = model.app;
const myAccountCtx = model.app.ctx;
2023-09-27 20:28:55 +00:00
// let socialPostsPanel: VNode | undefined;
// if (model.navigationModel.activeNav == "Social") {
// let focusedContentGetter = () => {
// // console.log("AppComponent:getFocusedContent before", Date.now() - t);
// let _ = getFocusedContent(
// model.social.focusedContent,
// app.conversationLists.convoSummaries,
// model.social.threads,
// );
// // 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 _;
// };
// let focusedContent = focusedContentGetter();
// console.log("AppComponent:getFocusedContent", Date.now() - t);
// socialPostsPanel = SocialPanel({
// allUsersInfo: app.conversationLists,
// ctx: app.ctx,
// db: app.database,
// emit: app.eventBus.emit,
// eventSyncer: app.eventSyncer,
// focusedContent: focusedContent,
// model: app.model,
// profileSyncer: app.profileSyncer,
// });
// console.debug("AppComponent:social done", Date.now() - t);
// }
2023-06-30 14:05:57 +00:00
let settingNode;
if (model.navigationModel.activeNav == "Setting") {
2023-06-30 14:05:57 +00:00
settingNode = (
<div
2023-09-07 00:37:14 +00:00
class={tw`flex-1 overflow-hidden overflow-y-auto`}
2023-06-30 14:05:57 +00:00
>
{Setting({
logout: app.logout,
relayConfig: app.relayConfig,
2023-06-30 14:05:57 +00:00
myAccountContext: myAccountCtx,
relayPool: props.pool,
emit: props.eventBus.emit,
2023-06-30 14:05:57 +00:00
})}
</div>
);
}
let appList;
if (model.navigationModel.activeNav == "AppList") {
2023-06-30 14:05:57 +00:00
appList = (
<div
class={tw`flex-1 overflow-hidden overflow-y-auto bg-[#313338]`}
>
<AppList />
</div>
);
}
let dmVNode;
let aboutNode;
if (
model.navigationModel.activeNav == "DM" ||
model.navigationModel.activeNav == "About"
2023-06-30 14:05:57 +00:00
) {
if (model.navigationModel.activeNav == "DM") {
2023-06-30 14:05:57 +00:00
dmVNode = (
<div
class={tw`flex-1 overflow-hidden`}
>
{DirectMessageContainer({
editors: model.editors,
...model.dm,
rightPanelModel: model.rightPanelModel,
emit: app.eventBus.emit,
2023-06-30 14:05:57 +00:00
myAccountContext: myAccountCtx,
db: app.database,
pool: props.pool,
allUserInfo: app.conversationLists,
profilesSyncer: app.profileSyncer,
eventSyncer: app.eventSyncer,
pinListGetter: app.otherConfig,
2023-06-30 14:05:57 +00:00
})}
</div>
);
}
if (model.navigationModel.activeNav == "About") {
2023-06-30 14:05:57 +00:00
aboutNode = About();
}
}
2023-07-30 06:39:53 +00:00
console.debug("AppComponent:2", Date.now() - t);
2023-06-30 14:05:57 +00:00
const final = (
<div
class={tw`font-roboto flex flex-col h-screen w-screen overflow-hidden`}
>
<div class={tw`w-full h-full flex flex-col`}>
<div class={tw`w-full flex-1 flex overflow-hidden`}>
<div class={tw`mobile:hidden`}>
{nav.NavBar({
profilePicURL: model.myProfile?.picture,
2023-06-30 14:05:57 +00:00
publicKey: myAccountCtx.publicKey,
database: app.database,
pool: props.pool,
emit: app.eventBus.emit,
...model.navigationModel,
2023-06-30 14:05:57 +00:00
})}
</div>
<div
class={tw`h-full px-[3rem] bg-[${SecondaryBackgroundColor}] flex-1 overflow-auto${
model.navigationModel.activeNav == "Profile" ? " block" : " hidden"
2023-06-30 14:05:57 +00:00
}`}
>
<div
class={tw`max-w-[35rem] h-full m-auto`}
>
{EditProfile({
emit: app.eventBus.emit,
myProfile: model.myProfile,
newProfileField: model.newProfileField,
2023-06-30 14:05:57 +00:00
})}
</div>
</div>
{dmVNode}
{aboutNode}
{settingNode}
2023-09-27 20:28:55 +00:00
{/* {socialPostsPanel} */}
2023-06-30 14:05:57 +00:00
{appList}
<Popover
inputChan={props.popOverInputChan}
/>
2023-06-30 14:05:57 +00:00
</div>
<div class={tw`desktop:hidden`}>
{
<nav.MobileNavBar
profilePicURL={model.myProfile?.picture}
publicKey={myAccountCtx.publicKey}
database={app.database}
pool={props.pool}
emit={app.eventBus.emit}
{...model.navigationModel}
/>
}
2023-06-30 14:05:57 +00:00
</div>
</div>
</div>
);
console.debug("AppComponent:end", Date.now() - t);
2023-06-30 14:05:57 +00:00
return final;
}
// todo: move to somewhere else
export function getFocusedContent(
focusedContent: PublicKey | NostrEvent | undefined,
2023-09-23 21:54:13 +00:00
allUserInfo: Map<string, ConversationSummary>,
2023-06-30 14:05:57 +00:00
threads: MessageThread[],
) {
if (focusedContent == undefined) {
return;
}
if (focusedContent instanceof PublicKey) {
2023-07-14 10:59:25 +00:00
const profileData = allUserInfo.get(focusedContent.hex)?.profile?.profile;
2023-06-30 14:05:57 +00:00
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,
};
}
}
}
}