blowater/UI/app.tsx

508 lines
17 KiB
TypeScript
Raw Normal View History

2023-06-30 14:05:57 +00:00
/** @jsx h */
import { h, render } from "https://esm.sh/preact@10.17.1";
2023-06-30 14:05:57 +00:00
import * as dm from "../features/dm.ts";
2023-10-07 20:40:18 +00:00
import { DirectMessageContainer } from "./dm.tsx";
2023-06-30 14:05:57 +00:00
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";
import { Datebase_View } from "../database.ts";
2023-10-15 22:39:21 +00:00
import { DM_List } from "./conversation-list.ts";
2023-10-07 20:40:18 +00:00
import { new_DM_EditorModel } from "./editor.tsx";
2023-06-30 14:05:57 +00:00
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";
2023-07-09 08:06:49 +00:00
import { getCurrentSignInCtx, setSignInState, SignIn } from "./signIn.tsx";
import { SecondaryBackgroundColor } from "./style/colors.ts";
import { EventSyncer } from "./event_syncer.ts";
import { 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";
2023-11-14 06:22:50 +00:00
import { Channel } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { group_GM_events, GroupChatSyncer, GroupMessageController } from "../features/gm.ts";
import { OtherConfig } from "./config-other.ts";
2023-10-07 20:40:18 +00:00
import { ProfileGetter } from "./search.tsx";
2023-12-22 12:20:34 +00:00
import { DirectedMessageController, InvalidEvent } from "../features/dm.ts";
2023-11-03 13:09:13 +00:00
import { ConnectionPool } from "../lib/nostr-ts/relay-pool.ts";
2023-11-18 17:11:07 +00:00
import { LamportTime } from "../time.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");
2023-11-27 13:35:01 +00:00
const installPrompt: nav.InstallPrompt = {
event: undefined,
};
window.addEventListener("beforeinstallprompt", async (event) => {
event.preventDefault();
installPrompt.event = event;
});
2023-11-25 14:29:50 +00:00
const lamport = new LamportTime();
const model = initialModel();
const eventBus = new EventBus<UI_Interaction_Event>();
const pool = new ConnectionPool();
const popOverInputChan: PopOverInputChannel = new Channel();
2023-11-17 07:06:11 +00:00
const dbView = await Datebase_View.New(database, database, database);
2023-11-25 13:26:03 +00:00
const newNostrEventChannel = new Channel<NostrEvent>();
(async () => {
for await (const event of newNostrEventChannel) {
const err = await pool.sendEvent(event);
if (err instanceof Error) {
console.error(err);
}
}
})();
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);
} else if (ctx) {
2023-11-25 14:29:50 +00:00
const otherConfig = await OtherConfig.FromLocalStorage(ctx, newNostrEventChannel, lamport);
const app = await App.Start({
database: dbView,
model,
ctx,
eventBus,
pool,
popOverInputChan,
otherConfig,
2023-11-25 14:29:50 +00:00
lamport,
2023-11-27 13:35:01 +00:00
installPrompt,
});
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,
2023-11-27 13:35:01 +00:00
installPrompt,
}),
document.body,
);
2023-07-21 12:40:49 +00:00
for await (
let _ of UI_Interaction_Update({
model,
eventBus,
dbView: dbView,
pool,
popOver: popOverInputChan,
2023-11-25 13:26:03 +00:00
newNostrEventChannel: newNostrEventChannel,
2023-11-25 14:29:50 +00:00
lamport,
2023-11-27 13:35:01 +00:00
installPrompt,
})
) {
2023-07-09 07:06:13 +00:00
const t = Date.now();
{
render(
AppComponent({
eventBus,
model,
pool,
popOverInputChan,
2023-11-27 13:35:01 +00:00
installPrompt,
}),
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 {
private constructor(
public readonly database: Datebase_View,
public readonly model: Model,
public readonly ctx: NostrAccountContext,
public readonly eventBus: EventBus<UI_Interaction_Event>,
public readonly pool: ConnectionPool,
public readonly popOverInputChan: PopOverInputChannel,
2023-10-01 23:16:08 +00:00
public readonly otherConfig: OtherConfig,
public readonly profileSyncer: ProfileSyncer,
public readonly eventSyncer: EventSyncer,
2023-10-15 22:39:21 +00:00
public readonly conversationLists: DM_List,
public readonly relayConfig: RelayConfig,
public readonly groupChatController: GroupMessageController,
public readonly lamport: time.LamportTime,
public readonly dmController: DirectedMessageController,
) {}
static async Start(args: {
database: Datebase_View;
model: Model;
ctx: NostrAccountContext;
eventBus: EventBus<UI_Interaction_Event>;
pool: ConnectionPool;
popOverInputChan: PopOverInputChannel;
otherConfig: OtherConfig;
2023-11-25 14:29:50 +00:00
lamport: LamportTime;
2023-11-27 13:35:01 +00:00
installPrompt: nav.InstallPrompt;
}) {
2023-11-25 14:29:50 +00:00
args.lamport.fromEvents(args.database.getAllEvents());
const eventSyncer = new EventSyncer(args.pool, args.database);
2023-11-18 17:11:07 +00:00
// init relay config
const relayConfig = await RelayConfig.FromLocalStorage({
ctx: args.ctx,
relayPool: args.pool,
});
console.log(relayConfig.getRelayURLs());
// init profile syncer
const profileSyncer = new ProfileSyncer(args.database, args.pool);
profileSyncer.add(args.ctx.publicKey.hex);
2023-11-18 17:11:07 +00:00
// init conversation list
2023-12-22 12:20:34 +00:00
const all_events = Array.from(args.database.getAllEvents());
2023-11-28 18:06:49 +00:00
const conversationLists = new DM_List(args.ctx);
2023-12-22 12:20:34 +00:00
const err = conversationLists.addEvents(all_events);
if (err instanceof InvalidEvent) {
console.error(err);
await args.database.remove(err.event.id);
}
2023-10-03 15:15:45 +00:00
const dmController = new DirectedMessageController(args.ctx);
const groupSyncer = new GroupChatSyncer(args.database, args.pool);
const groupChatController = new GroupMessageController(
args.ctx,
groupSyncer,
profileSyncer,
);
(async () => {
// load DMs
2023-12-22 12:20:34 +00:00
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);
await args.database.remove(e.id);
}
} else {
continue;
}
}
// load GMs
const group_events = await group_GM_events(args.ctx, Array.from(args.database.getAllEvents()));
for (const e of group_events.creataions) {
const error = await groupChatController.addEvent(e);
if (error instanceof Error) {
console.error(error, e);
await args.database.remove(e.id);
}
}
for (const e of group_events.invites) {
const error = await groupChatController.addEvent(e);
if (error instanceof Error) {
console.error(error, e);
await args.database.remove(e.id);
}
}
for (const e of group_events.messages) {
const error = await groupChatController.addEvent(e);
if (error instanceof Error) {
console.error(error, e);
await args.database.remove(e.id);
}
}
})();
2023-10-23 03:59:41 +00:00
const app = new App(
args.database,
args.model,
args.ctx,
args.eventBus,
args.pool,
args.popOverInputChan,
args.otherConfig,
profileSyncer,
eventSyncer,
conversationLists,
relayConfig,
groupChatController,
2023-11-25 14:29:50 +00:00
args.lamport,
dmController,
);
2023-11-27 13:35:01 +00:00
await app.initApp(args.installPrompt);
2023-10-23 03:59:41 +00:00
return app;
2023-06-30 14:05:57 +00:00
}
2023-11-27 13:35:01 +00:00
private initApp = async (installPrompt: nav.InstallPrompt) => {
2023-07-09 07:06:13 +00:00
console.log("App.initApp");
2023-11-25 13:26:03 +00:00
// configurations: pin list
(async () => {
const stream = await this.pool.newSub(OtherConfig.name, {
authors: [this.ctx.publicKey.hex],
2023-11-25 14:29:50 +00:00
kinds: [NostrKind.Encrypted_Custom_App_Data],
2023-11-25 13:26:03 +00:00
});
if (stream instanceof Error) {
throw stream; // crash the app
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
const ok = await this.database.addEvent(msg.res.event, msg.url);
if (ok instanceof Error) {
console.error(msg.res.event);
console.error(ok);
}
}
})();
// group chat synchronization
(async () => {
const stream = await this.pool.newSub("gm_send", {
authors: [this.ctx.publicKey.hex],
kinds: [NostrKind.Group_Message],
});
if (stream instanceof Error) {
throw stream; // crash the app
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
2023-11-16 09:18:30 +00:00
const ok = await this.database.addEvent(msg.res.event, msg.url);
if (ok instanceof Error) {
console.error(msg.res.event);
console.error(ok);
2023-10-04 15:07:40 +00:00
}
}
})();
(async () => {
const stream = await this.pool.newSub("gm_receive", {
"#p": [this.ctx.publicKey.hex],
kinds: [NostrKind.Group_Message],
});
if (stream instanceof Error) {
throw stream; // crash the app
}
for await (const msg of stream.chan) {
if (msg.res.type == "EOSE") {
continue;
}
2023-11-16 09:18:30 +00:00
const ok = await this.database.addEvent(msg.res.event, msg.url);
if (ok instanceof Error) {
console.error(msg.res.event);
console.error(ok);
}
}
})();
// Sync DM events
(async function sync_dm_events(
database: Datebase_View,
ctx: NostrAccountContext,
pool: ConnectionPool,
) {
const messageStream = dm.getAllEncryptedMessagesOf(
ctx.publicKey,
pool,
);
for await (const msg of messageStream) {
if (msg.res.type == "EVENT") {
2023-11-16 09:18:30 +00:00
const err = await database.addEvent(msg.res.event, msg.url);
if (err instanceof Error) {
console.log(err);
}
}
}
})(this.database, this.ctx, this.pool);
2023-06-30 14:05:57 +00:00
/* my profile */
const myProfileEvent = this.database.getProfilesByPublicKey(this.ctx.publicKey);
if (myProfileEvent != undefined) {
this.model.myProfile = myProfileEvent.profile;
}
2023-06-30 14:05:57 +00:00
/* contacts */
for (const contact of this.conversationLists.convoSummaries.values()) {
2023-10-21 11:29:47 +00:00
const editor = this.model.dmEditors.get(contact.pubkey.hex);
2023-06-30 14:05:57 +00:00
if (editor == null) {
const pubkey = PublicKey.FromHex(contact.pubkey.hex);
if (pubkey instanceof Error) {
throw pubkey; // impossible
}
2023-10-21 11:29:47 +00:00
this.model.dmEditors.set(
2023-06-30 14:05:57 +00:00
contact.pubkey.hex,
new_DM_EditorModel(
2023-06-30 14:05:57 +00:00
pubkey,
),
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
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.groupChatController,
this.dmController,
2023-10-15 22:39:21 +00:00
this.eventBus.emit,
2023-11-25 13:26:03 +00:00
{
otherConfig: this.otherConfig,
},
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,
2023-11-27 13:35:01 +00:00
installPrompt: installPrompt,
}),
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-11-27 13:35:01 +00:00
installPrompt: nav.InstallPrompt;
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");
2023-11-09 14:43:10 +00:00
return <SignIn emit={props.eventBus.emit} />;
2023-06-30 14:05:57 +00:00
}
const app = model.app;
const myAccountCtx = model.app.ctx;
2023-06-30 14:05:57 +00:00
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 = (
<DirectMessageContainer
{...model.dm}
rightPanelModel={model.rightPanelModel}
bus={app.eventBus}
ctx={myAccountCtx}
profileGetter={app.database}
pool={props.pool}
conversationLists={app.conversationLists}
profilesSyncer={app.profileSyncer}
eventSyncer={app.eventSyncer}
pinListGetter={app.otherConfig}
groupChatController={app.groupChatController}
newMessageChecker={app.conversationLists}
messageGetter={model.dm.isGroupMessage ? app.groupChatController : app.dmController}
newMessageListener={model.dm.isGroupMessage ? app.groupChatController : app.dmController}
relayRecordGetter={app.database}
/>
2023-06-30 14:05:57 +00:00
);
}
if (model.navigationModel.activeNav == "About") {
2023-12-17 20:41:49 +00:00
aboutNode = About(app.eventBus.emit);
2023-06-30 14:05:57 +00:00
}
}
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`h-screen w-full flex`}>
2023-12-21 17:17:03 +00:00
<nav.NavBar
publicKey={app.ctx.publicKey}
profileGetter={app.database}
emit={app.eventBus.emit}
installPrompt={props.installPrompt}
/>
<div
class={tw`h-full px-[3rem] sm:px-4 bg-[${SecondaryBackgroundColor}] flex-1 overflow-auto${
model.navigationModel.activeNav == "Profile" ? " block" : " hidden"
}`}
>
<div
class={tw`max-w-[35rem] h-full m-auto`}
>
<EditProfile
ctx={model.app.ctx}
profileGetter={app.database}
emit={props.eventBus.emit}
/>
2023-06-30 14:05:57 +00:00
</div>
</div>
2023-12-21 17:17:03 +00:00
{dmVNode}
{aboutNode}
{Setting({
show: model.navigationModel.activeNav == "Setting",
logout: app.logout,
relayConfig: app.relayConfig,
myAccountContext: myAccountCtx,
relayPool: props.pool,
emit: props.eventBus.emit,
})}
<Popover
inputChan={props.popOverInputChan}
/>
2023-06-30 14:05:57 +00:00
</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-10-07 20:40:18 +00:00
profileGetter: ProfileGetter,
2023-06-30 14:05:57 +00:00
) {
if (focusedContent == undefined) {
return;
}
if (focusedContent instanceof PublicKey) {
2023-10-07 20:40:18 +00:00
const profileData = profileGetter.getProfilesByPublicKey(focusedContent)?.profile;
2023-06-30 14:05:57 +00:00
return {
type: "ProfileData" as "ProfileData",
data: profileData,
pubkey: focusedContent,
};
}
}