import { Component, ComponentChildren, createRef, Fragment, h, RefObject, } from "https://esm.sh/preact@10.17.1"; import { PublicKey } from "../../libs/nostr.ts/key.ts"; import { RelayRecordGetter } from "../database.ts"; import { emitFunc } from "../event-bus.ts"; import { IconButtonClass } from "./components/tw.ts"; import { LeftArrowIcon } from "./icons/left-arrow-icon.tsx"; import { DirectMessagePanelUpdate, NameAndTime, ParseMessageContent, SyncEvent, Time, ViewUserDetail, } from "./message-panel.tsx"; import { ChatMessage, groupContinuousMessages, sortMessage } from "./message.ts"; import { ProfileGetter } from "./search.tsx"; import { SelectConversation } from "./search_model.ts"; import { sleep } from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { ProfileData } from "../features/profile.ts"; import { isMobile, setState } from "./_helper.ts"; import { Avatar } from "./components/avatar.tsx"; import { AboutIcon } from "./icons/about-icon.tsx"; import { BackgroundColor_MessagePanel, PrimaryTextColor } from "./style/colors.ts"; import { Parsed_Event } from "../nostr.ts"; import { NoteID } from "../../libs/nostr.ts/nip19.ts"; import { robohash } from "./relay-detail.tsx"; import { ReplyIcon } from "./icons/reply-icon.tsx"; interface Props { myPublicKey: PublicKey; messages: ChatMessage[]; emit: emitFunc; getters: { profileGetter: ProfileGetter; relayRecordGetter: RelayRecordGetter; getEventByID: func_GetEventByID; }; onReplyToEventIDChange?: (eventID?: string | NoteID) => void; } interface MessageListState { offset: number; } const ItemsOfPerPage = 50; export class MessageList extends Component { readonly messagesULElement = createRef(); state = { offset: 0, }; jitter = new JitterPrevention(100); async componentDidUpdate(previousProps: Readonly) { const newest = last(this.props.messages); const pre_newest = last(previousProps.messages); if ( newest && pre_newest && newest.author.hex == this.props.myPublicKey.hex && newest.event.id != pre_newest.event.id ) { await this.goToLastPage(); this.goToButtom(false); } } async componentDidMount() { const offset = this.props.messages.length - ItemsOfPerPage; await setState(this, { offset: offset <= 0 ? 0 : offset }); } render() { const messages_to_render = this.sortAndSliceMessage(); const groups = groupContinuousMessages(messages_to_render, (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 && !isReply(cur.event); }); const messageBoxGroups = []; for (const messages of groups) { const profileEvent = this.props.getters.profileGetter.getProfilesByPublicKey(messages[0].author); messageBoxGroups.push( MessageBoxGroup({ messages: messages, myPublicKey: this.props.myPublicKey, emit: this.props.emit, authorProfile: profileEvent ? profileEvent.profile : undefined, getters: this.props.getters, onReplyToEventIDChange: this.props.onReplyToEventIDChange, }), ); } return (
{MessageListView(this.goToButtom, this.messagesULElement, messageBoxGroups)}
); } sortAndSliceMessage = () => { return sortMessage(this.props.messages) .slice( this.state.offset, this.state.offset + ItemsOfPerPage, ); }; prePage = async () => { const offset = this.state.offset - ItemsOfPerPage / 2; if (offset > 0) { await setState(this, { offset }); } else { await setState(this, { offset: 0 }); } }; nextPage = async () => { const offset = this.state.offset + ItemsOfPerPage / 2; if (offset < this.props.messages.length) { await setState(this, { offset }); } else { await this.goToLastPage(); } }; goToButtom = (smooth: boolean) => { if (this.messagesULElement.current) { this.messagesULElement.current.scrollTo({ top: this.messagesULElement.current.scrollHeight, left: 0, behavior: smooth ? "smooth" : undefined, }); } }; goToLastPage = async () => { const newOffset = this.props.messages.length - ItemsOfPerPage / 2; await setState(this, { offset: newOffset > 0 ? newOffset : 0, }); console.log("goToLastPage", this.state.offset); }; } function MessageListView( goToButtom: (smooth: boolean) => void, messagesULElement: RefObject, messageBoxGroups: ComponentChildren, ) { return (
    {messageBoxGroups}
); } export class MessageList_V0 extends Component { readonly messagesULElement = createRef(); jitter = new JitterPrevention(100); async componentDidMount() { this.goToButtom(false); } componentDidUpdate(previousProps: Readonly): void { // todo: this is not a correct check of if new message is received // a better check is to see if the // current newest message is newer than previous newest message if (previousProps.messages.length < this.props.messages.length) { this.goToButtom(false); } } render() { const messages_to_render = this.sortAndSliceMessage(); const groups = groupContinuousMessages(messages_to_render, (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.getters.profileGetter .getProfilesByPublicKey(messages[0].author); messageBoxGroups.push( MessageBoxGroup({ messages: messages, myPublicKey: this.props.myPublicKey, emit: this.props.emit, authorProfile: profileEvent ? profileEvent.profile : undefined, getters: this.props.getters, }), ); } return MessageListView(this.goToButtom, this.messagesULElement, messageBoxGroups); } sortAndSliceMessage = () => { return sortMessage(this.props.messages); }; goToButtom = (smooth: boolean) => { if (this.messagesULElement.current) { this.messagesULElement.current.scrollTo({ top: this.messagesULElement.current.scrollHeight, left: 0, behavior: smooth ? "smooth" : undefined, }); } }; } 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; } } export type func_GetEventByID = ( id: string | NoteID, ) => Parsed_Event | undefined; function MessageBoxGroup(props: { onReplyToEventIDChange?: (eventID?: string | NoteID) => void; authorProfile: ProfileData | undefined; messages: ChatMessage[]; myPublicKey: PublicKey; emit: emitFunc< DirectMessagePanelUpdate | ViewUserDetail | SelectConversation | SyncEvent >; getters: { profileGetter: ProfileGetter; relayRecordGetter: RelayRecordGetter; getEventByID: func_GetEventByID; }; }) { const first_message = props.messages[0]; const rows = []; rows.push(
  • {MessageActions(first_message, props.emit, props.onReplyToEventIDChange)} {renderRelply(first_message.event, props.getters, props.emit)}
    { props.emit({ type: "ViewUserDetail", pubkey: first_message.author, }); }} />
    {NameAndTime( first_message.author, props.authorProfile, props.myPublicKey, first_message.created_at, )}
                    {ParseMessageContent(
                       first_message,
                        props.emit,
                        props.getters,
                        )}
                        
  • , ); for (let i = 1; i < props.messages.length; i++) { const msg = props.messages[i]; rows.push(
  • {MessageActions(msg, props.emit, props.onReplyToEventIDChange)} {Time(msg.created_at)}
                        {ParseMessageContent(msg, props.emit, props.getters)}
                        
  • , ); } const vnode = (
      {rows}
    ); return vnode; } function MessageActions( message: ChatMessage, emit: emitFunc, onReplyToEventIDChange?: (eventID?: string | NoteID) => void, ) { return (
    {onReplyToEventIDChange ? ( ) : null}
    ); } function last(array: Array): T | undefined { if (array.length == 0) { return undefined; } else { return array[array.length - 1]; } } function isReply(event: Parsed_Event) { return event.parsedTags.reply || event.parsedTags.root || event.parsedTags.e.length != 0; } function renderRelply(event: Parsed_Event, getters: { getEventByID: func_GetEventByID; profileGetter: ProfileGetter; }, emit: emitFunc) { if (!isReply(event)) return; const replyEventId = event.parsedTags.reply?.[0] || event.parsedTags.root?.[0] || event.parsedTags.e[0]; const reply_to_event = getters.getEventByID(replyEventId); if (!reply_to_event) { return ; } let author = reply_to_event.publicKey.bech32(); let picture = robohash(reply_to_event.publicKey.hex); if (reply_to_event.pubkey) { const profile = getters.profileGetter.getProfilesByPublicKey(reply_to_event.publicKey); if (profile) { author = profile.profile.name || profile.profile.display_name || reply_to_event?.publicKey.bech32(); picture = profile.profile.picture || robohash(reply_to_event.publicKey.hex); } } return ( ); } function ReplyTo( props: { unknown?: false; reply: { pubkey: PublicKey; content: string; name: string; picture: string; }; emit: emitFunc; } | { unknown: true; noteId: NoteID; }, ) { return (
    {props.unknown ? (
    {props.noteId.bech32()}
    ) : ( <>
    props.emit({ type: "ViewUserDetail", pubkey: props.reply.pubkey, })} >
    {props.reply.content}
    )}
    ); }