/** @jsx h */ import { Component, ComponentChildren, createRef, h } from "https://esm.sh/preact@10.17.1"; import { tw } from "https://esm.sh/twind@0.16.16"; import { Editor, EditorEvent, EditorModel } from "./editor.tsx"; import { AboutIcon, CloseIcon, LeftArrowIcon, ReplyIcon } from "./icons/mod.tsx"; import { Avatar } from "./components/avatar.tsx"; import { DividerClass, IconButtonClass } from "./components/tw.ts"; import { sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { emitFunc, EventEmitter } from "../event-bus.ts"; import { ChatMessage, groupContinuousMessages, sortMessage, urlIsImage } from "./message.ts"; import { PublicKey } from "../lib/nostr-ts/key.ts"; import { NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts"; import { CustomAppData_Event, PinContact, PlainText_Nostr_Event, Profile_Nostr_Event, UnpinContact, } from "../nostr.ts"; import { ProfileData, ProfilesSyncer } from "../features/profile.ts"; import { MessageThread } from "./dm.tsx"; import { UserDetail } from "./user-detail.tsx"; import { MessageThreadPanel } from "./message-thread-panel.tsx"; import { Database_Contextual_View } from "../database.ts"; import { HoverButtonBackgroudColor, LinkColor, PrimaryTextColor } from "./style/colors.ts"; import { getUserInfoFromPublicKey, UserInfo } from "./contact-list.ts"; import { EventSyncer } from "./event_syncer.ts"; import { ButtonGroup } from "./components/button-group.tsx"; export type RightPanelModel = { show: boolean; }; export type DirectMessagePanelUpdate = | { type: "ToggleRightPanel"; show: boolean; } | ViewThread | ViewUserDetail | { type: "ViewEventDetail"; event: PlainText_Nostr_Event; }; export type ViewThread = { type: "ViewThread"; root: NostrEvent; }; export type ViewUserDetail = { type: "ViewUserDetail"; pubkey: PublicKey; }; interface DirectMessagePanelProps { myPublicKey: PublicKey; editorModel: EditorModel; messages: MessageThread[]; focusedContent: { type: "MessageThread"; data: MessageThread; editor: EditorModel; } | { type: "ProfileData"; data?: ProfileData; pubkey: PublicKey; } | undefined; rightPanelModel: RightPanelModel; db: Database_Contextual_View; eventEmitter: EventEmitter< EditorEvent | DirectMessagePanelUpdate | PinContact | UnpinContact >; profilesSyncer: ProfilesSyncer; eventSyncer: EventSyncer; allUserInfo: Map; } // export function MessagePanel(props: DirectMessagePanelProps) { export class MessagePanel extends Component { render() { const props = this.props; const t = Date.now(); let placeholder = "Post your thoughts"; if (props.editorModel.target.kind == NostrKind.DIRECT_MESSAGE) { placeholder = `Message @${ props.editorModel.target.receiver.name || props.editorModel.target.receiver.pubkey.bech32() }`; } let rightPanel; if (props.rightPanelModel.show) { let rightPanelChildren: h.JSX.Element | undefined; if (props.focusedContent) { if (props.focusedContent.type == "MessageThread") { rightPanelChildren = ( ); } else if (props.focusedContent.type == "ProfileData") { rightPanelChildren = ( ); } } rightPanel = ( {rightPanelChildren} ); } let vnode = (
{ } { }
{!props.rightPanelModel.show ? ( ) : undefined} {rightPanel}
); console.log("DirectMessagePanel:end", Date.now() - t); return vnode; } } interface MessageListProps { myPublicKey: PublicKey; threads: MessageThread[]; db: Database_Contextual_View; eventEmitter: EventEmitter; profilesSyncer: ProfilesSyncer; eventSyncer: EventSyncer; allUserInfo: Map; } interface MessageListState { currentRenderCount: number; } const ItemsOfPerPage = 100; export class MessageList extends Component { constructor(public props: MessageListProps) { super(); } messagesULElement = createRef(); state = { currentRenderCount: ItemsOfPerPage, }; jitter = new JitterPrevention(100); componentWillReceiveProps() { this.setState({ currentRenderCount: ItemsOfPerPage, }); } onScroll = async (e: h.JSX.TargetedUIEvent) => { if ( e.currentTarget.scrollHeight - e.currentTarget.offsetHeight + e.currentTarget.scrollTop < 1000 ) { const ok = await this.jitter.shouldExecute(); if (!ok || this.state.currentRenderCount >= this.props.threads.length) { return; } this.setState({ currentRenderCount: Math.min( this.state.currentRenderCount + ItemsOfPerPage, this.props.threads.length, ), }); } }; sortAndSliceMessage = () => { return sortMessage(this.props.threads) .slice( 0, this.state.currentRenderCount, ); }; render() { const t = Date.now(); const groups = groupContinuousMessages(this.sortAndSliceMessage(), (pre, cur) => { const sameAuthor = pre.root.event.pubkey == cur.root.event.pubkey; const _66sec = Math.abs(cur.root.created_at.getTime() - pre.root.created_at.getTime()) < 1000 * 60; return sameAuthor && _66sec; }); console.log("MessageList:groupContinuousMessages", Date.now() - t); const messageBoxGroups = []; let i = 0; for (const threads of groups) { messageBoxGroups.push( MessageBoxGroup({ messageGroup: threads.map((thread) => { i++; return { msg: thread.root, replyCount: thread.replies.length, }; }), myPublicKey: this.props.myPublicKey, eventEmitter: this.props.eventEmitter, db: this.props.db, profilesSyncer: this.props.profilesSyncer, eventSyncer: this.props.eventSyncer, allUserInfo: this.props.allUserInfo, }), ); } console.log(`MessageList:elements ${i}`, Date.now() - t); const vNode = (
    {messageBoxGroups}
); console.log("MessageList:end", Date.now() - t); return vNode; } } function MessageBoxGroup(props: { messageGroup: { msg: ChatMessage; replyCount: number; }[]; myPublicKey: PublicKey; db: Database_Contextual_View; allUserInfo: Map; eventEmitter: EventEmitter; profilesSyncer: ProfilesSyncer; eventSyncer: EventSyncer; }) { // const t = Date.now(); const messageGroups = props.messageGroup.reverse(); if (messageGroups.length == 0) { return; } const first_group = messageGroups[0]; const rows = []; rows.push(
  • {MessageActions(first_group.msg.event, props.eventEmitter.emit)} { props.eventEmitter.emit({ type: "ViewUserDetail", pubkey: first_group.msg.event.publicKey, }); }} />
    {NameAndTime( first_group.msg.event.publicKey, getUserInfoFromPublicKey(first_group.msg.event.publicKey, props.allUserInfo) ?.profile?.profile, 0, props.myPublicKey, first_group.msg.created_at, )}
                                    {ParseMessageContent(
                                        first_group.msg,
                                        props.allUserInfo,
                                        props.profilesSyncer,
                                        props.eventSyncer,
                                        props.eventEmitter,
                                        )}
                    
    {first_group.replyCount > 0 ? (
    { props.eventEmitter.emit({ type: "ViewThread", root: first_group.msg.event, }); }} > {first_group.replyCount} replies
    ) : undefined}
  • , ); for (let i = 1; i < messageGroups.length; i++) { const msg = messageGroups[i]; rows.push(
  • {MessageActions(msg.msg.event, props.eventEmitter.emit)} {Time(msg.msg.created_at)}
    {NameAndTime( msg.msg.event.publicKey, getUserInfoFromPublicKey(msg.msg.event.publicKey, props.allUserInfo) ?.profile?.profile, i, props.myPublicKey, msg.msg.created_at, )}
                        {ParseMessageContent(
                            msg.msg,
                            props.allUserInfo,
                            props.profilesSyncer,
                            props.eventSyncer,
                            props.eventEmitter,
                            )}
                        
    {msg.replyCount > 0 ? (
    { props.eventEmitter.emit({ type: "ViewThread", root: msg.msg.event, }); }} > {msg.replyCount} replies
    ) : undefined}
  • , ); } const vnode = (
      {rows}
    ); // console.log("MessageBoxGroup", Date.now() - t); return vnode; } function MessageActions( event: PlainText_Nostr_Event, emit: emitFunc, ) { return ( ); } export function Time(created_at: Date) { return (
    ); } export function NameAndTime( author: PublicKey, author_profile: ProfileData | undefined, index: number, myPublicKey: PublicKey, created_at: Date, ) { if (index === 0) { let show = author.bech32(); if (author.hex == myPublicKey.hex) { show = "Me"; } else if (author_profile?.name) { show = author_profile.name; } return (

    {show}

    {created_at.toLocaleString()}

    ); } } export function ParseMessageContent( message: ChatMessage, allUserInfo: Map, profilesSyncer: ProfilesSyncer, eventSyncer: EventSyncer, eventEmitter: EventEmitter, ) { if (message.type == "image") { return ; } const vnode = []; let start = 0; for (const item of message.event.parsedContentItems) { vnode.push(message.content.slice(start, item.start)); const itemStr = message.content.slice(item.start, item.end + 1); switch (item.type) { case "url": { if (urlIsImage(itemStr)) { vnode.push(); } else { vnode.push( {itemStr} , ); } } break; case "npub": { const userInfo = allUserInfo.get(item.pubkey); if (userInfo) { const profile = userInfo.profile; if (profile) { vnode.push( ProfileCard( profile.profile, PublicKey.FromHex(item.pubkey) as PublicKey, eventEmitter, ), ); break; } else { profilesSyncer.add(item.pubkey); } } else { profilesSyncer.add(item.pubkey); } vnode.push( ProfileCard( undefined, PublicKey.FromHex(item.pubkey) as PublicKey, eventEmitter, ), ); } break; case "note": { const event = eventSyncer.syncEvent(item.noteID); if (event instanceof Promise) { break; } vnode.push(NoteCard(event, eventEmitter, allUserInfo)); } break; case "tag": // todo break; } start = item.end + 1; } vnode.push(message.content.slice(start)); return vnode; } function ProfileCard( profile: ProfileData | undefined, pubkey: PublicKey, eventEmitter: EventEmitter, ) { return (
    { eventEmitter.emit({ type: "ViewUserDetail", pubkey: pubkey, }); }} >

    {profile?.name || pubkey.bech32()}

    {profile?.about}

    ); } function NoteCard( event: Profile_Nostr_Event | PlainText_Nostr_Event | CustomAppData_Event, eventEmitter: EventEmitter, allUserInfo: Map, ) { switch (event.kind) { case NostrKind.META_DATA: return ProfileCard(event.profile, event.publicKey, eventEmitter); case NostrKind.TEXT_NOTE: case NostrKind.DIRECT_MESSAGE: const profile = allUserInfo.get(event.pubkey)?.profile; return (

    {profile?.profile.name || event.publicKey.bech32()}

    {event.content}

    ); default: return (
    {event.content}
    ); } } type RightPanelProps = { eventEmitter: EventEmitter; rightPanelModel: RightPanelModel; children: ComponentChildren; }; function RightPanel(props: RightPanelProps) { return (
    {props.children}
    ); } class JitterPrevention { constructor(private duration: number) {} cancel: ((value: void) => void) | undefined; async shouldExecute(): Promise { if (this.cancel) { this.cancel(); this.cancel = undefined; return this.shouldExecute(); } const p = new Promise((resolve) => { this.cancel = resolve; }); const cancelled = await sleep(this.duration, p); return !cancelled; } }