blowater/app/UI/app_update.tsx

616 lines
21 KiB
TypeScript
Raw Normal View History

/** @jsx h */
import { ComponentChildren, h } from "https://esm.sh/preact@10.17.1";
import {
Channel,
closed,
PopChannel,
PutChannel,
sleep,
} from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts";
2024-01-01 17:28:10 +00:00
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { NoteID } from "../../libs/nostr.ts/nip19.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";
2023-10-15 22:39:21 +00:00
import { emitFunc, EventBus } from "../event-bus.ts";
import { DirectedMessageController, sendDirectMessages } from "../features/dm.ts";
import { saveProfile } from "../features/profile.ts";
import {
2023-09-12 14:51:27 +00:00
Encrypted_Event,
getTags,
Parsed_Event,
PinConversation,
Profile_Nostr_Event,
UnpinConversation,
} from "../nostr.ts";
2024-01-01 17:28:10 +00:00
import { LamportTime } from "../time.ts";
import { App } from "./app.tsx";
import { Model } from "./app_model.ts";
import { PopOverInputChannel } from "./components/popover.tsx";
2023-10-01 23:16:08 +00:00
import { OtherConfig } from "./config-other.ts";
2024-01-01 17:28:10 +00:00
import { DM_List } from "./conversation-list.ts";
import { ContactUpdate } from "./conversation-list.tsx";
import { StartInvite } from "./dm.tsx";
2023-10-23 13:57:58 +00:00
import { SaveProfile } from "./edit-profile.tsx";
import { EditorEvent, SendMessage } from "./editor.tsx";
2024-01-01 17:28:10 +00:00
import { EventDetail, EventDetailItem } from "./event-detail.tsx";
2024-01-01 17:28:10 +00:00
import { DirectMessagePanelUpdate } from "./message-panel.tsx";
import { ChatMessage } from "./message.ts";
import { InstallPrompt, NavigationModel, NavigationUpdate, SelectRelay } from "./nav.tsx";
2024-01-01 17:28:10 +00:00
import { notify } from "./notification.ts";
import { RelayInformationComponent } from "./relay-detail.tsx";
2024-01-01 17:28:10 +00:00
import { Search } from "./search.tsx";
import { SearchUpdate, SelectConversation } from "./search_model.ts";
import { RelayConfigChange, ViewRecommendedRelaysList, ViewRelayDetail } from "./setting.tsx";
import { SignInEvent } from "./sign-in.ts";
import { TagSelected } from "./contact-tags.tsx";
import { BlockUser, UnblockUser, UserDetail } from "./user-detail.tsx";
import { RelayRecommendList } from "./relay-recommend-list.tsx";
import { HidePopOver } from "./components/popover.tsx";
import { Social_Model } from "./channel-container.tsx";
import { SyncEvent } from "./message-panel.tsx";
2024-03-15 15:18:36 +00:00
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";
2024-03-19 10:27:18 +00:00
import { generateTags } from "./editor.ts";
2023-06-30 14:05:57 +00:00
export type UI_Interaction_Event =
| SearchUpdate
| ContactUpdate
| EditorEvent
| NavigationUpdate
| DirectMessagePanelUpdate
| BackToChannelList
2023-06-30 14:05:57 +00:00
| BackToContactList
2023-10-23 13:57:58 +00:00
| SaveProfile
| PinConversation
| UnpinConversation
2023-07-27 08:51:53 +00:00
| SignInEvent
2023-09-22 16:27:36 +00:00
| RelayConfigChange
2023-10-19 03:44:43 +00:00
| StartInvite
| ViewRelayDetail
| ViewRecommendedRelaysList
2024-01-02 12:42:11 +00:00
| TagSelected
| BlockUser
| UnblockUser
| SelectRelay
| HidePopOver
| SyncEvent;
2023-06-30 14:05:57 +00:00
type BackToChannelList = {
type: "BackToChannelList";
};
2023-06-30 14:05:57 +00:00
type BackToContactList = {
type: "BackToContactList";
};
export type AppEventBus = EventBus<UI_Interaction_Event>;
2024-01-02 12:42:11 +00:00
export type UserBlocker = {
blockUser(pubkey: PublicKey): void;
unblockUser(pubkey: PublicKey): void;
isUserBlocked(pubkey: PublicKey): boolean;
getBlockedUsers(): Set<string>;
};
2023-06-30 14:05:57 +00:00
/////////////////////
// UI Interfaction //
/////////////////////
export function UI_Interaction_Update(args: {
model: Model;
eventBus: AppEventBus;
dbView: Database_View;
pool: ConnectionPool;
popOver: PopOverInputChannel;
rightPanel: Channel<() => ComponentChildren>;
newNostrEventChannel: Channel<NostrEvent>;
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: {
2023-08-03 09:13:16 +00:00
model: Model;
eventBus: AppEventBus;
dbView: Database_View;
2023-08-03 09:13:16 +00:00
pool: ConnectionPool;
popOver: PopOverInputChannel;
rightPanel: Channel<() => ComponentChildren>;
2023-11-25 13:26:03 +00:00
newNostrEventChannel: Channel<NostrEvent>;
2023-11-25 14:29:50 +00:00
lamport: LamportTime;
2023-11-27 13:35:01 +00:00
installPrompt: InstallPrompt;
toastInputChan: ToastChannel;
}) => {
2023-11-27 13:35:01 +00:00
const { model, dbView, eventBus, pool, installPrompt } = args;
for await (const event of eventBus.onChange()) {
2023-06-30 14:05:57 +00:00
console.log(event);
if (event.type == "SignInEvent") {
const ctx = event.ctx;
if (ctx) {
console.log("sign in as", ctx.publicKey.bech32());
const otherConfig = await OtherConfig.FromLocalStorage(
ctx,
args.newNostrEventChannel,
args.lamport,
);
const app = await App.Start({
database: dbView,
model,
ctx,
eventBus,
pool,
popOverInputChan: args.popOver,
rightPanelInputChan: args.rightPanel,
otherConfig,
lamport: args.lamport,
installPrompt,
toastInputChan: args.toastInputChan,
});
model.app = app;
} else {
console.error("failed to sign in");
}
continue;
2023-07-09 07:06:13 +00:00
}
const app = model.app;
if (app == undefined) { // if not signed in
console.warn(event, "is not valid before signing");
console.warn("This could not happen!");
continue;
}
2024-03-15 14:30:58 +00:00
const current_relay = pool.getRelay(model.currentRelay);
if (current_relay == undefined) {
console.error(Array.from(pool.getRelays()));
continue;
}
const blowater_relay = pool.getRelay(default_blowater_relay);
if (blowater_relay == undefined) {
console.error(Array.from(pool.getRelays()));
continue;
}
// All events below are only valid after signning in
if (event.type == "SelectRelay") {
model.currentRelay = event.relay.url;
} //
// Searchx
2023-06-30 14:05:57 +00:00
//
else if (event.type == "HidePopOver") {
app.popOverInputChan.put({
children: undefined,
});
2023-06-30 14:05:57 +00:00
} else if (event.type == "StartSearch") {
app.database.getProfilesByText;
const search = (
<Search
placeholder={`Search a user's public key or name (${app.database.getUniqueProfileCount()} profiles)`}
2024-03-16 07:09:30 +00:00
profileGetter={app.database}
emit={eventBus.emit}
/>
);
args.popOver.put({ children: search });
2023-06-30 14:05:57 +00:00
} //
//
2023-11-21 07:20:39 +00:00
// Setting
//
else if (event.type == "ViewRelayDetail") {
app.popOverInputChan.put({
children: (
<RelayInformationComponent
relayUrl={event.url}
profileGetter={app.database}
emit={app.eventBus.emit}
/>
),
});
} else if (event.type == "ViewRecommendedRelaysList") {
app.popOverInputChan.put({
children: (
<RelayRecommendList
2024-03-16 13:01:10 +00:00
relayConfig={app.relayConfig}
emit={eventBus.emit}
/>
),
2023-11-21 07:20:39 +00:00
});
} //
//
2023-06-30 14:05:57 +00:00
// Contacts
//
2023-09-23 21:33:30 +00:00
else if (event.type == "SelectConversation") {
console.log("SelectConversation", event.pubkey.hex);
2023-12-17 20:41:49 +00:00
model.navigationModel.activeNav = "DM";
model.dm.currentConversation = event.pubkey;
app.popOverInputChan.put({ children: undefined });
app.conversationLists.markRead(event.pubkey);
2023-06-30 14:05:57 +00:00
} else if (event.type == "BackToContactList") {
model.dm.currentConversation = undefined;
} else if (event.type == "PinConversation") {
2023-11-25 13:26:03 +00:00
const err1 = await app.otherConfig.addPin(event.pubkey);
if (err1 instanceof Error) {
console.error(err1);
continue;
}
} else if (event.type == "UnpinConversation") {
2023-11-25 13:26:03 +00:00
const err1 = await app.otherConfig.removePin(event.pubkey);
if (err1 instanceof Error) {
console.error(err1);
continue;
}
2023-06-30 14:05:57 +00:00
} //
//
// Editor
//
else if (event.type == "SendMessage") {
2023-12-04 18:14:26 +00:00
handle_SendMessage(
event,
app.ctx,
app.lamport,
app.database,
{
...model,
current_relay,
blowater_relay,
},
2023-12-04 18:14:26 +00:00
).then((res) => {
if (res instanceof Error) {
console.error(res);
2024-03-15 15:18:36 +00:00
app.toastInputChan.put(
SendingEventRejection(eventBus.emit, current_relay.url, res.message),
);
} else {
chan.put(true);
2023-12-04 18:14:26 +00:00
}
});
2023-06-30 14:05:57 +00:00
} else if (event.type == "UpdateMessageFiles") {
console.log("to be implemented");
} else if (event.type == "UpdateEditorText") {
console.log("to be implemented");
2023-06-30 14:05:57 +00:00
} //
//
2023-10-23 13:57:58 +00:00
// Profile
2023-06-30 14:05:57 +00:00
//
2023-10-23 13:57:58 +00:00
else if (event.type == "SaveProfile") {
2024-03-17 17:05:32 +00:00
if (event.profile == undefined) {
2024-03-15 15:18:36 +00:00
app.toastInputChan.put(() => "profile is empty");
2024-03-15 14:30:58 +00:00
} else {
2024-03-17 17:05:32 +00:00
saveProfile(
2024-03-15 14:30:58 +00:00
event.profile,
event.ctx,
current_relay,
2024-03-17 17:05:32 +00:00
).then((result) => {
app.popOverInputChan.put({ children: undefined });
if (result instanceof Error) {
app.toastInputChan.put(
SendingEventRejection(eventBus.emit, current_relay.url, result.message),
);
} else {
app.toastInputChan.put(() => "profile has been updated");
}
});
2024-03-15 14:30:58 +00:00
}
2023-06-30 14:05:57 +00:00
} //
//
// Navigation
//
else if (event.type == "ChangeNavigation") {
2023-11-02 13:10:16 +00:00
model.navigationModel.activeNav = event.id;
2023-06-30 14:05:57 +00:00
} //
//
// Channel
2023-06-30 14:05:57 +00:00
//
else if (event.type == "SelectChannel") {
model.social.relaySelectedChannel.set(model.currentRelay, event.channel);
app.popOverInputChan.put({ children: undefined });
} else if (event.type == "BackToChannelList") {
model.social.relaySelectedChannel.delete(model.currentRelay);
app.popOverInputChan.put({ children: undefined });
} //
// DM
//
else if (event.type == "ViewUserDetail") {
app.rightPanelInputChan.put(
() => {
return (
<UserDetail
targetUserProfile={app.database.getProfilesByPublicKey(event.pubkey)
?.profile ||
{}}
pubkey={event.pubkey}
emit={eventBus.emit}
// dmList={app.conversationLists}
blocked={app.conversationLists.isUserBlocked(event.pubkey)}
/>
);
2023-10-26 08:42:04 +00:00
},
);
} else if (event.type == "OpenNote") {
open(`https://nostrapp.link/#${NoteID.FromHex(event.event.id).bech32()}?select=true`);
2023-10-07 17:15:27 +00:00
} else if (event.type == "StartInvite") {
app.popOverInputChan.put({
children: <div></div>,
});
} else if (event.type == "RelayConfigChange") {
2024-03-16 13:01:10 +00:00
(async () => {
if (event.kind == "add") {
const relay = await app.relayConfig.add(event.url);
if (relay instanceof Error) {
console.error(relay);
const msg = relay.message;
app.toastInputChan.put(() => msg);
}
2024-03-16 13:01:10 +00:00
} else {
const err = await app.relayConfig.remove(event.url);
if (err instanceof Error) {
app.toastInputChan.put(() => err.message);
return;
}
if (current_relay.url == event.url) {
model.currentRelay = default_blowater_relay;
}
2024-03-16 13:01:10 +00:00
}
})();
} else if (event.type == "ViewEventDetail") {
const nostrEvent = event.message.event;
const eventID = nostrEvent.id;
const eventIDBech32 = NoteID.FromString(nostrEvent.id).bech32();
const authorPubkey = event.message.author;
const content = nostrEvent.content;
const originalEventRaw = JSON.stringify(
{
content: nostrEvent.content,
created_at: nostrEvent.created_at,
kind: nostrEvent.kind,
tags: nostrEvent.tags,
pubkey: nostrEvent.pubkey,
id: nostrEvent.id,
sig: nostrEvent.sig,
},
null,
4,
);
const items: EventDetailItem[] = [
{
title: "Event ID",
fields: [
eventID,
eventIDBech32,
],
},
{
title: "Author",
fields: [
authorPubkey.hex,
authorPubkey.bech32(),
],
},
{
title: "Relays",
fields: Array.from(app.database.getRelayRecord(nostrEvent.id)),
},
];
if (nostrEvent.kind == NostrKind.DIRECT_MESSAGE) {
items.push({
title: "Content",
fields: [
content,
2023-10-21 11:29:47 +00:00
event.message.content,
originalEventRaw,
],
});
} else {
items.push({
title: "Content",
fields: [
event.message.content,
originalEventRaw,
],
});
}
app.popOverInputChan.put({
children: (
<EventDetail
items={items}
/>
),
});
2024-01-02 12:42:11 +00:00
} else if (event.type == "BlockUser") {
app.conversationLists.blockUser(event.pubkey);
} else if (event.type == "UnblockUser") {
app.conversationLists.unblockUser(event.pubkey);
} else if (event.type == "SyncEvent") {
for (const relay of app.pool.getRelays()) {
relay.getEvent(event.eventID).then((nostr_event) => {
if (nostr_event instanceof Error) {
console.error(nostr_event);
return;
}
if (nostr_event) {
app.database.addEvent(nostr_event, relay.url);
}
});
}
continue;
} else {
console.log(event, "is not handled");
continue;
2023-06-30 14:05:57 +00:00
}
await chan.put(true);
2023-06-30 14:05:57 +00:00
}
};
2023-06-30 14:05:57 +00:00
export type DirectMessageGetter = ChatMessagesGetter & {
getDirectMessageStream(publicKey: string): Channel<ChatMessage>;
};
export type ChatMessagesGetter = {
getChatMessages(publicKey: string): ChatMessage[];
};
2023-06-30 14:05:57 +00:00
//////////////
// Database //
//////////////
export async function* Database_Update(
ctx: NostrAccountContext,
database: Database_View,
2023-06-30 14:05:57 +00:00
model: Model,
lamport: LamportTime,
2023-10-15 22:39:21 +00:00
convoLists: DM_List,
dmController: DirectedMessageController,
2023-10-15 22:39:21 +00:00
emit: emitFunc<SelectConversation>,
2023-11-25 13:26:03 +00:00
args: {
otherConfig: OtherConfig;
},
2023-06-30 14:05:57 +00:00
) {
const changes = database.subscribe();
2023-06-30 14:05:57 +00:00
while (true) {
2024-01-01 17:28:10 +00:00
await sleep(333);
2023-06-30 14:05:57 +00:00
await changes.ready();
const changes_events: (Encrypted_Event | Profile_Nostr_Event | Parsed_Event)[] = [];
2023-06-30 14:05:57 +00:00
while (true) {
if (!changes.isReadyToPop()) {
break;
}
const e = await changes.pop();
2024-01-01 17:28:10 +00:00
if (e == closed) {
2023-06-30 14:05:57 +00:00
console.error("unreachable: db changes channel should never close");
break;
}
changes_events.push(e.event);
2023-06-30 14:05:57 +00:00
}
2023-12-22 13:03:59 +00:00
convoLists.addEvents(changes_events, true);
2023-06-30 14:05:57 +00:00
for (let e of changes_events) {
const t = getTags(e).lamport_timestamp;
if (t) {
lamport.set(t);
}
if (e.kind == NostrKind.META_DATA || e.kind == NostrKind.DIRECT_MESSAGE) {
2024-03-17 17:05:32 +00:00
if (e.kind == NostrKind.DIRECT_MESSAGE) {
console.log("add event");
const err = await dmController.addEvent({
...e,
kind: e.kind,
});
if (err instanceof Error) {
console.error(err);
}
console.log("add event done");
}
2023-11-25 14:29:50 +00:00
} else if (e.kind == NostrKind.Encrypted_Custom_App_Data) {
2023-11-25 13:26:03 +00:00
console.log(e);
const err = await args.otherConfig.addEvent(e);
if (err instanceof Error) {
console.error(err);
}
2023-06-30 14:05:57 +00:00
}
2023-10-15 22:39:21 +00:00
// notification should be moved to after domain objects
{
const author = database.getProfilesByPublicKey(e.publicKey)
?.profile;
if (e.pubkey != ctx.publicKey.hex && e.parsedTags.p.includes(ctx.publicKey.hex)) {
notify(
author?.name ? author.name : "",
"new message",
author?.picture ? author.picture : "",
() => {
if (e.kind == NostrKind.DIRECT_MESSAGE) {
const k = PublicKey.FromHex(e.pubkey);
if (k instanceof Error) {
console.error(k);
return;
}
emit({
type: "SelectConversation",
pubkey: k,
});
} else if (e.kind == NostrKind.TEXT_NOTE) {
// todo
// open the default kind 1 app
} else {
// todo
// handle other types
}
},
);
}
}
2023-06-30 14:05:57 +00:00
}
yield model;
}
}
export async function handle_SendMessage(
event: SendMessage,
ctx: NostrAccountContext,
lamport: LamportTime,
db: Database_View,
args: {
navigationModel: NavigationModel;
social: Social_Model;
dm: {
currentConversation: PublicKey | undefined;
};
blowater_relay: SingleRelayConnection;
current_relay: SingleRelayConnection;
},
) {
if (event.text.length == 0) {
return new Error("can't send empty message");
}
let events: NostrEvent[];
if (args.navigationModel.activeNav == "DM") {
const events_send = await sendDirectMessages({
sender: ctx,
receiverPublicKey: args.dm.currentConversation as PublicKey,
message: event.text,
files: event.files,
lamport_timestamp: lamport.now(),
eventSender: args.blowater_relay,
2024-03-19 10:27:18 +00:00
tags: generateTags(event.text),
});
if (events_send instanceof Error) {
return events_send;
}
for (const event of events_send) {
const result = args.current_relay.sendEvent(event);
if (result instanceof Error) {
return result;
}
}
events = events_send;
} else if (args.navigationModel.activeNav == "Public") {
const nostr_event = await prepareNormalNostrEvent(ctx, {
content: event.text,
kind: NostrKind.TEXT_NOTE,
2024-03-19 10:27:18 +00:00
tags: generateTags(event.text),
});
const err = await args.current_relay.sendEvent(nostr_event);
if (err instanceof Error) {
return err;
}
events = [nostr_event];
} else {
return new Error(`${args.navigationModel.activeNav} should not send messages`);
}
for (const eventSent of events) {
const err = await db.addEvent(eventSent, undefined);
if (err instanceof Error) {
console.error(err);
}
}
}