/** @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 { Avatar } from "./components/avatar.tsx"; import { IconButtonClass } from "./components/tw.ts"; import { Channel, sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { emitFunc } from "../event-bus.ts"; import { ChatMessage, groupContinuousMessages, parseContent, sortMessage, urlIsImage } from "./message.ts"; import { PublicKey } from "../lib/nostr-ts/key.ts"; import { NostrEvent, NostrKind } from "../lib/nostr-ts/nostr.ts"; import { Parsed_Event, PinConversation, UnpinConversation } from "../nostr.ts"; import { ProfileData, ProfileSyncer } from "../features/profile.ts"; 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"; import { InviteCard } from "./invite-card.tsx"; import { SelectConversation } from "./search_model.ts"; import { AboutIcon } from "./icons/about-icon.tsx"; import { CloseIcon } from "./icons/close-icon.tsx"; import { LeftArrowIcon } from "./icons/left-arrow-icon.tsx"; import { NoteID } from "../lib/nostr-ts/nip19.ts"; import { ChatMessagesGetter } from "./app_update.tsx"; export type RightPanelModel = { show: boolean; }; export type DirectMessagePanelUpdate = | { type: "ToggleRightPanel"; show: boolean; } | ViewThread | ViewUserDetail | ViewNoteThread | { type: "ViewEventDetail"; message: ChatMessage; }; export type ViewNoteThread = { type: "ViewNoteThread"; event: NostrEvent; }; export type ViewThread = { type: "ViewThread"; root: NostrEvent; }; export type ViewUserDetail = { type: "ViewUserDetail"; pubkey: PublicKey; }; interface DirectMessagePanelProps { myPublicKey: PublicKey; isGroupMessage: boolean; editorModel: EditorModel; focusedContent: { type: "ProfileData"; data?: ProfileData; pubkey: PublicKey; } | undefined; rightPanelModel: RightPanelModel; emit: emitFunc< EditorEvent | DirectMessagePanelUpdate | PinConversation | UnpinConversation | SelectConversation >; profilesSyncer: ProfileSyncer; eventSyncer: EventSyncer; profileGetter: ProfileGetter; newMessageListener: NewMessageListener; messageGetter: ChatMessagesGetter; } export type NewMessageListener = { onChange(): Channel; }; export class MessagePanel extends Component { async componentDidMount() { const changes = this.props.newMessageListener.onChange(); for (;;) { await sleep(333); await changes.ready(); for (;;) { if (changes.isReadyToPop()) { await changes.pop(); continue; } break; } this.setState({}); } } render() { const props = this.props; 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; messages: ChatMessage[]; 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.messages.length) { return; } this.setState({ currentRenderCount: Math.min( this.state.currentRenderCount + ItemsOfPerPage, this.props.messages.length, ), }); } }; sortAndSliceMessage = () => { return sortMessage(this.props.messages) .slice( 0, this.state.currentRenderCount, ); }; render() { const t = Date.now(); const groups = groupContinuousMessages(this.sortAndSliceMessage(), (pre, cur) => { const sameAuthor = pre.event.pubkey == cur.event.pubkey; const _66sec = Math.abs(cur.created_at.getTime() - pre.created_at.getTime()) < 1000 * 60; return sameAuthor && _66sec; }); const messageBoxGroups = []; for (const messages of groups) { const profileEvent = this.props.profileGetter.getProfilesByPublicKey(messages[0].author); messageBoxGroups.push( MessageBoxGroup({ messages: messages, myPublicKey: this.props.myPublicKey, emit: this.props.emit, profilesSyncer: this.props.profilesSyncer, eventSyncer: this.props.eventSyncer, authorProfile: profileEvent ? profileEvent.profile : undefined, profileGetter: this.props.profileGetter, }), ); } const vNode = (
    {messageBoxGroups}
); console.log("MessageList:end", Date.now() - t); return vNode; } } function MessageBoxGroup(props: { authorProfile: ProfileData | undefined; messages: ChatMessage[]; myPublicKey: PublicKey; emit: emitFunc; profilesSyncer: ProfileSyncer; eventSyncer: EventSyncer; profileGetter: ProfileGetter; }) { const messageGroups = props.messages.reverse(); if (messageGroups.length == 0) { return; } const first_group = messageGroups[0]; const rows = []; rows.push(
  • {MessageActions(first_group, props.emit)} { props.emit({ type: "ViewUserDetail", pubkey: first_group.author, }); }} />
    {NameAndTime( first_group.author, props.authorProfile, props.myPublicKey, first_group.created_at, )}
                        {ParseMessageContent(
                            first_group,
                            props.authorProfile,
                            props.profilesSyncer,
                            props.eventSyncer,
                            props.emit,
                            props.profileGetter,
                            )}
                    
  • , ); for (let i = 1; i < messageGroups.length; i++) { const msg = messageGroups[i]; rows.push(
  • {MessageActions(msg, props.emit)} {Time(msg.created_at)}
                        {ParseMessageContent(
                            msg,
                            props.authorProfile,
                            props.profilesSyncer,
                            props.eventSyncer,
                            props.emit,
                            props.profileGetter
                            )}
                        
  • , ); } const vnode = (
      {rows}
    ); // console.log("MessageBoxGroup", Date.now() - t); return vnode; } function MessageActions( message: ChatMessage, 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, authorProfile: ProfileData | undefined, profilesSyncer: ProfileSyncer, eventSyncer: EventSyncer, emit: emitFunc, profileGetter: ProfileGetter, ) { if (message.type == "image") { return ; } let parsedContentItems; if (message.type == "gm_invitation") { return ( ); } else if (message.event.kind == NostrKind.Group_Message) { parsedContentItems = parseContent(message.content); } else { parsedContentItems = message.event.parsedContentItems; } const vnode = []; let start = 0; for (const item of 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": { if (authorProfile) { const profile = profileGetter.getProfilesByPublicKey(item.pubkey); vnode.push( , ); break; } 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; } const profile = profileGetter.getProfilesByPublicKey(event.publicKey); vnode.push(Card(event, profile ? profile.profile : undefined, emit)); } break; case "nevent": { const event = eventSyncer.syncEvent(NoteID.FromString(item.event.pointer.id)); if ( event instanceof Promise || event.kind == NostrKind.DIRECT_MESSAGE ) { vnode.push(itemStr); break; } const profile = profileGetter.getProfilesByPublicKey(event.publicKey); vnode.push(Card(event, profile ? profile.profile : undefined, emit)); } case "tag": // todo break; } start = item.end + 1; } vnode.push(message.content.slice(start)); return vnode; } function Card( event: Parsed_Event, authorProfile: ProfileData | undefined, emit: emitFunc, ) { switch (event.kind) { case NostrKind.META_DATA: return ; case NostrKind.TEXT_NOTE: 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; } }