/** @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 { IconButtonClass } from "./components/tw.ts"; import { sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { emitFunc } 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 { DirectedMessage_Event, Parsed_Event, PinConversation, Profile_Nostr_Event, Text_Note_Event, UnpinConversation, } from "../nostr.ts"; import { ProfileData, ProfileSyncer } from "../features/profile.ts"; import { MessageThread } from "./dm.tsx"; import { UserDetail } from "./user-detail.tsx"; import { LinkColor, PrimaryTextColor } from "./style/colors.ts"; import { EventSyncer } from "./event_syncer.ts"; import { ButtonGroup } from "./components/button-group.tsx"; import { ProfileCard } from "./profile-card.tsx"; import { NoteCard } from "./note-card.tsx"; import { ProfileGetter } from "./search.tsx"; export type RightPanelModel = { show: boolean; }; export type DirectMessagePanelUpdate = | { type: "ToggleRightPanel"; show: boolean; } | ViewThread | ViewUserDetail | ViewNoteThread | { type: "ViewEventDetail"; event: Text_Note_Event | DirectedMessage_Event; }; export type ViewNoteThread = { type: "ViewNoteThread"; event: Parsed_Event; }; export type ViewThread = { type: "ViewThread"; root: NostrEvent; }; export type ViewUserDetail = { type: "ViewUserDetail"; pubkey: PublicKey; }; interface DirectMessagePanelProps { myPublicKey: PublicKey; isGroupChat: boolean; editorModel: EditorModel; messages: MessageThread[]; focusedContent: { type: "ProfileData"; data?: ProfileData; pubkey: PublicKey; } | undefined; rightPanelModel: RightPanelModel; emit: emitFunc< EditorEvent | DirectMessagePanelUpdate | PinConversation | UnpinConversation >; profilesSyncer: ProfileSyncer; eventSyncer: EventSyncer; profileGetter: ProfileGetter; } export class MessagePanel extends Component { render() { const props = this.props; const t = Date.now(); let rightPanel; if (props.rightPanelModel.show) { let rightPanelChildren: h.JSX.Element | undefined; if (props.focusedContent) { if (props.focusedContent.type == "ProfileData") { rightPanelChildren = ( ); } } rightPanel = ( {rightPanelChildren} ); } let vnode = (
{!props.rightPanelModel.show ? ( ) : undefined} {rightPanel}
); return vnode; } } interface MessageListProps { myPublicKey: PublicKey; threads: MessageThread[]; emit: emitFunc; profilesSyncer: ProfileSyncer; eventSyncer: EventSyncer; profileGetter: ProfileGetter; } 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; }); 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, emit: this.props.emit, profilesSyncer: this.props.profilesSyncer, eventSyncer: this.props.eventSyncer, profileGetter: this.props.profileGetter, }), ); } 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; emit: emitFunc; profilesSyncer: ProfileSyncer; profileGetter: ProfileGetter; 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.emit)} { props.emit({ type: "ViewUserDetail", pubkey: first_group.msg.event.publicKey, }); }} />
    {NameAndTime( first_group.msg.event.publicKey, props.profileGetter.getProfilesByPublicKey(first_group.msg.event.publicKey) ?.profile, props.myPublicKey, first_group.msg.created_at, )}
                                    {ParseMessageContent(
                                        first_group.msg,
                                        props.profilesSyncer,
                                        props.profileGetter,
                                        props.eventSyncer,
                                        props.emit,
                                        )}
                    
    {first_group.replyCount > 0 ? (
    { props.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.emit)} {Time(msg.msg.created_at)}
                        {ParseMessageContent(
                            msg.msg,
                            props.profilesSyncer,
                            props.profileGetter,
                            props.eventSyncer,
                            props.emit,
                            )}
                        
    {msg.replyCount > 0 ? (
    { props.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: Text_Note_Event | DirectedMessage_Event, emit: emitFunc, ) { return ( ); } export function Time(created_at: Date) { return (
    ); } export function NameAndTime( author: PublicKey, author_profile: ProfileData | undefined, myPublicKey: PublicKey, created_at: Date, ) { 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, profilesSyncer: ProfileSyncer, profileGetter: ProfileGetter, eventSyncer: EventSyncer, emit: emitFunc, ) { 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 = profileGetter.getProfilesByPublicKey(item.pubkey); if (userInfo) { const profile = userInfo.profile; if (profile) { vnode.push( , ); break; } else { profilesSyncer.add(item.pubkey.hex); } } else { profilesSyncer.add(item.pubkey.hex); } vnode.push( , ); } break; case "note": { const event = eventSyncer.syncEvent(item.noteID); if ( event instanceof Promise || event.kind == NostrKind.DIRECT_MESSAGE ) { vnode.push(itemStr); break; } vnode.push(Card(event, emit, profileGetter)); } break; case "tag": // todo break; } start = item.end + 1; } vnode.push(message.content.slice(start)); return vnode; } function Card( event: Profile_Nostr_Event | Text_Note_Event, emit: emitFunc, profileGetter: ProfileGetter, ) { switch (event.kind) { case NostrKind.META_DATA: return ; case NostrKind.TEXT_NOTE: const pubkey = PublicKey.FromHex(event.pubkey); // @ts-ignore const profile = profileGetter.getProfilesByPublicKey(pubkey)?.profile; return ; } } type RightPanelProps = { emit: emitFunc; 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; } }