mirror of
https://github.com/BlowaterNostr/blowater.git
synced 2024-10-18 15:43:20 +00:00
761 lines
27 KiB
TypeScript
761 lines
27 KiB
TypeScript
/** @jsx h */
|
|
import { h } from "https://esm.sh/preact@10.17.1";
|
|
import {
|
|
Channel,
|
|
closed,
|
|
PutChannel,
|
|
sleep,
|
|
} from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
|
|
import { prepareDeletionEvent, prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts";
|
|
import { prepareReactionEvent } from "../../libs/nostr.ts/nip25.ts";
|
|
import { prepareReplyEvent } from "../nostr.ts";
|
|
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";
|
|
import { emitFunc, EventBus } from "../event-bus.ts";
|
|
import { DirectedMessageController, sendDirectMessages } from "../features/dm.ts";
|
|
import { saveProfile } from "../features/profile.ts";
|
|
import {
|
|
Encrypted_Event,
|
|
getTags,
|
|
Parsed_Event,
|
|
PinConversation,
|
|
Profile_Nostr_Event,
|
|
Tag,
|
|
UnpinConversation,
|
|
} from "../nostr.ts";
|
|
import { LamportTime } from "../time.ts";
|
|
import { App } from "./app.tsx";
|
|
import { Model, rememberActiveNav, rememberCurrentRelay } from "./app_model.ts";
|
|
import { PopOverInputChannel } from "./components/popover.tsx";
|
|
import { OtherConfig } from "./config-other.ts";
|
|
import { DM_List } from "./conversation-list.ts";
|
|
import { ContactUpdate } from "./conversation-list.tsx";
|
|
import { StartInvite } from "./dm.tsx";
|
|
import { SaveProfile } from "./edit-profile.tsx";
|
|
import { EditorEvent, SendMessage } from "./editor.tsx";
|
|
import { EventDetail, EventDetailItem } from "./event-detail.tsx";
|
|
|
|
import { DirectMessagePanelUpdate, SendReaction } from "./message-panel.tsx";
|
|
import { ChatMessage, parseContent } from "./message.ts";
|
|
import { InstallPrompt, NavigationModel, NavigationUpdate, SelectRelay } from "./nav.tsx";
|
|
import { notify } from "./notification.ts";
|
|
import { SpaceSetting } from "./relay-detail.tsx";
|
|
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 { Public_Model } from "./public-message-container.tsx";
|
|
import { SyncEvent } from "./message-panel.tsx";
|
|
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";
|
|
import { DeleteEvent, func_GetEventByID } from "./message-list.tsx";
|
|
import { FilterContent } from "./filter.tsx";
|
|
import { CloseRightPanel } from "./components/right-panel.tsx";
|
|
import { RightPanelChannel } from "./components/right-panel.tsx";
|
|
import { ReplyToMessage } from "./message-list.tsx";
|
|
import { EditorSelectProfile } from "./editor.tsx";
|
|
import { uploadFile } from "../../libs/nostr.ts/nip96.ts";
|
|
|
|
export type UI_Interaction_Event =
|
|
| SearchUpdate
|
|
| ContactUpdate
|
|
| EditorEvent
|
|
| NavigationUpdate
|
|
| DirectMessagePanelUpdate
|
|
| BackToContactList
|
|
| SaveProfile
|
|
| PinConversation
|
|
| UnpinConversation
|
|
| SignInEvent
|
|
| RelayConfigChange
|
|
| StartInvite
|
|
| ViewRelayDetail
|
|
| ViewRecommendedRelaysList
|
|
| TagSelected
|
|
| BlockUser
|
|
| UnblockUser
|
|
| SelectRelay
|
|
| HidePopOver
|
|
| SyncEvent
|
|
| FilterContent
|
|
| CloseRightPanel
|
|
| ReplyToMessage
|
|
| EditorSelectProfile
|
|
| DeleteEvent
|
|
| SendReaction;
|
|
|
|
type BackToContactList = {
|
|
type: "BackToContactList";
|
|
};
|
|
export type AppEventBus = EventBus<UI_Interaction_Event>;
|
|
|
|
export type UserBlocker = {
|
|
blockUser(pubkey: PublicKey): void;
|
|
unblockUser(pubkey: PublicKey): void;
|
|
isUserBlocked(pubkey: PublicKey): boolean;
|
|
getBlockedUsers(): Set<string>;
|
|
};
|
|
|
|
/////////////////////
|
|
// UI Interfaction //
|
|
/////////////////////
|
|
export function UI_Interaction_Update(args: {
|
|
model: Model;
|
|
eventBus: AppEventBus;
|
|
dbView: Database_View;
|
|
popOver: PopOverInputChannel;
|
|
rightPanel: RightPanelChannel;
|
|
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: {
|
|
model: Model;
|
|
eventBus: AppEventBus;
|
|
dbView: Database_View;
|
|
popOver: PopOverInputChannel;
|
|
rightPanel: RightPanelChannel;
|
|
lamport: LamportTime;
|
|
installPrompt: InstallPrompt;
|
|
toastInputChan: ToastChannel;
|
|
}) => {
|
|
const { model, dbView, eventBus, installPrompt } = args;
|
|
for await (const event of eventBus.onChange()) {
|
|
console.log(event);
|
|
if (event.type == "SignInEvent") {
|
|
const ctx = event.ctx;
|
|
console.log("sign in as", ctx.publicKey.bech32());
|
|
const otherConfig = await OtherConfig.FromLocalStorage(
|
|
ctx,
|
|
args.lamport,
|
|
);
|
|
const app = await App.Start({
|
|
database: dbView,
|
|
model,
|
|
ctx,
|
|
eventBus,
|
|
pool: new ConnectionPool({ signer: ctx }),
|
|
popOverInputChan: args.popOver,
|
|
rightPanelInputChan: args.rightPanel,
|
|
otherConfig,
|
|
lamport: args.lamport,
|
|
installPrompt,
|
|
toastInputChan: args.toastInputChan,
|
|
});
|
|
model.app = app;
|
|
continue;
|
|
}
|
|
|
|
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;
|
|
}
|
|
const pool = app.pool;
|
|
const blowater_relay = pool.getRelay(default_blowater_relay);
|
|
if (blowater_relay == undefined) {
|
|
console.error(Array.from(pool.getRelays()));
|
|
continue;
|
|
}
|
|
|
|
let current_relay = pool.getRelay(model.currentRelay);
|
|
if (current_relay == undefined) {
|
|
current_relay = blowater_relay;
|
|
rememberCurrentRelay(default_blowater_relay);
|
|
}
|
|
|
|
// All events below are only valid after signning in
|
|
if (event.type == "SelectRelay") {
|
|
rememberCurrentRelay(event.relay.url);
|
|
model.currentRelay = event.relay.url;
|
|
} //
|
|
// Searchx
|
|
//
|
|
else if (event.type == "HidePopOver") {
|
|
app.popOverInputChan.put({
|
|
children: undefined,
|
|
});
|
|
} else if (event.type == "StartSearch") {
|
|
app.database.getProfilesByText;
|
|
const search = (
|
|
<Search
|
|
placeholder={`Search a user's public key or name (${app.database.getUniqueProfileCount()} profiles)`}
|
|
profileGetter={app.database}
|
|
emit={eventBus.emit}
|
|
/>
|
|
);
|
|
args.popOver.put({ children: search });
|
|
} //
|
|
//
|
|
// Setting
|
|
//
|
|
else if (event.type == "ViewRelayDetail") {
|
|
const relay = pool.getRelay(event.url);
|
|
if (relay) {
|
|
app.popOverInputChan.put({
|
|
children: (
|
|
<SpaceSetting
|
|
profileGetter={app.database}
|
|
emit={app.eventBus.emit}
|
|
relay={relay}
|
|
getMemberSet={app.database.getMemberSet}
|
|
getProfileByPublicKey={app.database.getProfileByPublicKey}
|
|
/>
|
|
),
|
|
});
|
|
} else {
|
|
console.error(event.url, "is not in the pool");
|
|
}
|
|
} else if (event.type == "ViewRecommendedRelaysList") {
|
|
app.popOverInputChan.put({
|
|
children: (
|
|
<RelayRecommendList
|
|
relayConfig={app.relayConfig}
|
|
emit={eventBus.emit}
|
|
getRelayRecommendations={app.database.getRelayRecommendations}
|
|
/>
|
|
),
|
|
});
|
|
} //
|
|
//
|
|
// Contacts
|
|
//
|
|
else if (event.type == "SelectConversation") {
|
|
console.log("SelectConversation", event.pubkey.hex);
|
|
rememberActiveNav("DM");
|
|
model.navigationModel.activeNav = "DM";
|
|
model.dm.currentConversation = event.pubkey;
|
|
app.popOverInputChan.put({ children: undefined });
|
|
app.conversationLists.markRead(event.pubkey);
|
|
} else if (event.type == "BackToContactList") {
|
|
model.dm.currentConversation = undefined;
|
|
} else if (event.type == "PinConversation") {
|
|
const err1 = await app.otherConfig.addPin(event.pubkey);
|
|
if (err1 instanceof Error) {
|
|
console.error(err1);
|
|
continue;
|
|
}
|
|
} else if (event.type == "UnpinConversation") {
|
|
const err1 = await app.otherConfig.removePin(event.pubkey);
|
|
if (err1 instanceof Error) {
|
|
console.error(err1);
|
|
continue;
|
|
}
|
|
} //
|
|
//
|
|
// Editor
|
|
//
|
|
else if (event.type == "SendMessage") {
|
|
if (event.text.length == 0) {
|
|
app.toastInputChan.put(() => "the message is empty");
|
|
continue;
|
|
}
|
|
handle_SendMessage(
|
|
event,
|
|
app.ctx,
|
|
app.lamport,
|
|
app.database,
|
|
{
|
|
...model,
|
|
current_relay,
|
|
blowater_relay,
|
|
getEventByID: app.database.getEventByID,
|
|
},
|
|
).then((res) => {
|
|
if (res instanceof Error) {
|
|
console.error(res);
|
|
app.toastInputChan.put(
|
|
SendingEventRejection(eventBus.emit, current_relay.url, res.message),
|
|
);
|
|
} else {
|
|
chan.put(true);
|
|
}
|
|
});
|
|
} else if (event.type == "UploadImage") {
|
|
app.toastInputChan.put(() => "Uploading image...");
|
|
const api_url = "https://nostr.build/api/v2/nip96/upload";
|
|
const uploaded = await uploadFile(app.ctx, { api_url, file: event.file });
|
|
app.toastInputChan.put(() => uploaded.message);
|
|
event.callback(uploaded);
|
|
} //
|
|
//
|
|
// Profile
|
|
//
|
|
else if (event.type == "SaveProfile") {
|
|
if (event.profile == undefined) {
|
|
app.toastInputChan.put(() => "profile is empty");
|
|
} else {
|
|
saveProfile(
|
|
event.profile,
|
|
event.ctx,
|
|
current_relay,
|
|
).then((result: string | Error) => {
|
|
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");
|
|
}
|
|
});
|
|
}
|
|
} //
|
|
//
|
|
// Navigation
|
|
//
|
|
else if (event.type == "ChangeNavigation") {
|
|
rememberActiveNav(event.id);
|
|
model.navigationModel.activeNav = event.id;
|
|
} else if (event.type == "CloseRightPanel") {
|
|
app.rightPanelInputChan.put(undefined);
|
|
} //
|
|
//
|
|
// Deletion
|
|
//
|
|
else if (event.type == "DeleteEvent") {
|
|
const deletionEvent = await prepareDeletionEvent(
|
|
app.ctx,
|
|
"Request deletion",
|
|
event.event,
|
|
);
|
|
if (deletionEvent instanceof Error) {
|
|
app.toastInputChan.put(() => deletionEvent.message);
|
|
continue;
|
|
}
|
|
current_relay.sendEvent(deletionEvent).then((res) => {
|
|
if (res instanceof Error) {
|
|
app.toastInputChan.put(() => res.message);
|
|
}
|
|
});
|
|
} //
|
|
//
|
|
// DM
|
|
//
|
|
else if (event.type == "ViewUserDetail") {
|
|
sync_user_detail_data({ pool, pubkey: event.pubkey, database: app.database, profile_only: true });
|
|
app.rightPanelInputChan.put(
|
|
() => {
|
|
return (
|
|
<UserDetail
|
|
targetUserProfile={app.database.getProfileByPublicKey(event.pubkey)
|
|
?.profile ||
|
|
{}}
|
|
pubkey={event.pubkey}
|
|
emit={eventBus.emit}
|
|
// dmList={app.conversationLists}
|
|
blocked={app.conversationLists.isUserBlocked(event.pubkey)}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
} else if (event.type == "OpenNote") {
|
|
open(`https://nostrapp.link/#${NoteID.FromHex(event.event.id).bech32()}?select=true`);
|
|
} else if (event.type == "StartInvite") {
|
|
app.popOverInputChan.put({
|
|
children: <div></div>,
|
|
});
|
|
} else if (event.type == "RelayConfigChange") {
|
|
(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);
|
|
}
|
|
} 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;
|
|
}
|
|
}
|
|
})();
|
|
} else if (event.type == "SendReaction") {
|
|
const { event: targetEvent, content } = event;
|
|
const reactionEvent = await prepareReactionEvent(app.ctx, { targetEvent, content });
|
|
if (reactionEvent instanceof Error) {
|
|
await app.toastInputChan.put(() => reactionEvent.message);
|
|
continue;
|
|
}
|
|
current_relay.sendEvent(reactionEvent).then((res) => {
|
|
if (res instanceof Error) {
|
|
app.toastInputChan.put(() => res.message);
|
|
}
|
|
});
|
|
} 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 reactions = app.database.getReactionEvents(nostrEvent.id);
|
|
|
|
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,
|
|
event.message.content,
|
|
originalEventRaw,
|
|
],
|
|
});
|
|
} else {
|
|
items.push({
|
|
title: "Content",
|
|
fields: [
|
|
event.message.content,
|
|
originalEventRaw,
|
|
],
|
|
});
|
|
}
|
|
if (reactions.size > 0) {
|
|
items.push({
|
|
title: "Reactions",
|
|
fields: Array.from(reactions).map((r) => {
|
|
return `${r.id}: ${r.content}`;
|
|
}),
|
|
});
|
|
}
|
|
|
|
app.popOverInputChan.put({
|
|
children: (
|
|
<EventDetail
|
|
items={items}
|
|
/>
|
|
),
|
|
});
|
|
} 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 if (event.type == "FilterContent") {
|
|
const trimmed = event.content.trim();
|
|
if (trimmed.length == 63) {
|
|
const pubkey = PublicKey.FromBech32(trimmed);
|
|
if (pubkey instanceof PublicKey) {
|
|
sync_user_detail_data({ pool, pubkey, database: app.database });
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
} else {
|
|
console.log(event, "is not handled");
|
|
continue;
|
|
}
|
|
await chan.put(true);
|
|
}
|
|
};
|
|
|
|
export type DirectMessageGetter = ChatMessagesGetter & {
|
|
getDirectMessageStream(publicKey: string): Channel<ChatMessage>;
|
|
};
|
|
|
|
export type ChatMessagesGetter = {
|
|
getChatMessages(publicKey: string): ChatMessage[];
|
|
getMessageById(id: string): ChatMessage | undefined;
|
|
};
|
|
|
|
//////////////
|
|
// Database //
|
|
//////////////
|
|
export async function* Database_Update(
|
|
ctx: NostrAccountContext,
|
|
database: Database_View,
|
|
model: Model,
|
|
lamport: LamportTime,
|
|
convoLists: DM_List,
|
|
dmController: DirectedMessageController,
|
|
emit: emitFunc<SelectConversation>,
|
|
args: {
|
|
otherConfig: OtherConfig;
|
|
},
|
|
) {
|
|
const changes = database.subscribe();
|
|
while (true) {
|
|
await sleep(333);
|
|
await changes.ready();
|
|
const changes_events: (Encrypted_Event | Profile_Nostr_Event | Parsed_Event)[] = [];
|
|
while (true) {
|
|
if (!changes.isReadyToPop()) {
|
|
break;
|
|
}
|
|
const e = await changes.pop();
|
|
if (e == closed) {
|
|
console.error("unreachable: db changes channel should never close");
|
|
break;
|
|
}
|
|
changes_events.push(e.event);
|
|
}
|
|
|
|
convoLists.addEvents(changes_events, true);
|
|
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) {
|
|
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");
|
|
}
|
|
} else if (e.kind == NostrKind.Encrypted_Custom_App_Data) {
|
|
console.log(e);
|
|
const err = await args.otherConfig.addEvent(e);
|
|
if (err instanceof Error) {
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// notification should be moved to after domain objects
|
|
{
|
|
const author = database.getProfileByPublicKey(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
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
yield model;
|
|
}
|
|
}
|
|
|
|
export async function handle_SendMessage(
|
|
ui_event: SendMessage,
|
|
ctx: NostrAccountContext,
|
|
lamport: LamportTime,
|
|
db: Database_View,
|
|
args: {
|
|
navigationModel: NavigationModel;
|
|
public: Public_Model;
|
|
dm: {
|
|
currentConversation: PublicKey | undefined;
|
|
};
|
|
blowater_relay: SingleRelayConnection;
|
|
current_relay: SingleRelayConnection;
|
|
getEventByID: func_GetEventByID;
|
|
},
|
|
) {
|
|
if (ui_event.text.length == 0) {
|
|
return new Error("can't send empty message");
|
|
}
|
|
|
|
let replyToEvent: NostrEvent | undefined;
|
|
if (ui_event.reply_to_event_id) {
|
|
replyToEvent = args.getEventByID(ui_event.reply_to_event_id);
|
|
}
|
|
|
|
let events: NostrEvent[];
|
|
if (args.navigationModel.activeNav == "DM") {
|
|
const events_send = await sendDirectMessages({
|
|
sender: ctx,
|
|
receiverPublicKey: args.dm.currentConversation as PublicKey,
|
|
message: ui_event.text,
|
|
files: ui_event.files,
|
|
lamport_timestamp: lamport.now(),
|
|
eventSender: args.blowater_relay,
|
|
targetEvent: replyToEvent,
|
|
});
|
|
if (events_send instanceof Error) {
|
|
return events_send;
|
|
}
|
|
events = events_send;
|
|
} else if (args.navigationModel.activeNav == "Public") {
|
|
const tags = generateTags({
|
|
content: ui_event.text,
|
|
getEventByID: args.getEventByID,
|
|
current_relay: args.current_relay.url,
|
|
});
|
|
const nostr_event = replyToEvent
|
|
? await prepareReplyEvent(
|
|
ctx,
|
|
{
|
|
targetEvent: replyToEvent,
|
|
tags: [...tags, ["client", "https://blowater.app"]],
|
|
content: ui_event.text,
|
|
currentRelay: args.current_relay.url,
|
|
},
|
|
)
|
|
: await prepareNormalNostrEvent(ctx, {
|
|
content: ui_event.text,
|
|
kind: NostrKind.TEXT_NOTE,
|
|
tags: [...tags, ["client", "https://blowater.app"]],
|
|
});
|
|
if (nostr_event instanceof Error) {
|
|
return nostr_event;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function generateTags(
|
|
args: {
|
|
content: string;
|
|
getEventByID: func_GetEventByID;
|
|
current_relay: string;
|
|
},
|
|
) {
|
|
const eTags = new Map<string, [string, string]>();
|
|
const pTags = new Set<string>();
|
|
const parsedTextItems = parseContent(args.content);
|
|
for (const item of parsedTextItems) {
|
|
if (item.type === "nevent") {
|
|
eTags.set(item.nevent.pointer.id, [item.nevent.pointer.relays?.[0] || "", "mention"]);
|
|
if (item.nevent.pointer.pubkey) {
|
|
pTags.add(item.nevent.pointer.pubkey.hex);
|
|
}
|
|
} else if (item.type === "npub") {
|
|
pTags.add(item.pubkey.hex);
|
|
} else if (item.type === "note") {
|
|
eTags.set(item.noteID.hex, [args.current_relay, "mention"]);
|
|
const event = args.getEventByID(item.noteID);
|
|
if (event) {
|
|
pTags.add(event.pubkey);
|
|
}
|
|
}
|
|
}
|
|
let tags: Tag[] = [];
|
|
eTags.forEach((v, k) => {
|
|
tags.push(["e", k, v[0], v[1]]);
|
|
});
|
|
pTags.forEach((v) => {
|
|
tags.push(["p", v]);
|
|
});
|
|
return tags;
|
|
}
|
|
|
|
async function sync_user_detail_data(
|
|
args: { pool: ConnectionPool; pubkey: PublicKey; database: Database_View; profile_only?: boolean },
|
|
) {
|
|
const kinds = [NostrKind.META_DATA];
|
|
if (!args.profile_only) {
|
|
kinds.push(NostrKind.TEXT_NOTE, NostrKind.Long_Form);
|
|
}
|
|
const sub = await args.pool.newSub(args.pubkey.bech32(), {
|
|
kinds,
|
|
authors: [args.pubkey.hex],
|
|
});
|
|
if (sub instanceof Error) {
|
|
console.error(sub);
|
|
await args.pool.closeSub(args.pubkey.bech32());
|
|
return;
|
|
}
|
|
for await (const msg of sub.chan) {
|
|
if (msg.res.type == "EOSE") {
|
|
break;
|
|
} else if (msg.res.type == "EVENT") {
|
|
await args.database.addEvent(msg.res.event, msg.url);
|
|
}
|
|
}
|
|
await args.pool.closeSub(args.pubkey.bech32());
|
|
}
|