render replies (#432)

Co-authored-by: bob2402 <dv4xd@proton.me>
This commit is contained in:
BlowaterNostr 2024-03-21 14:04:32 +08:00 committed by GitHub
parent 689b7bd019
commit 1ab5e65191
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,4 +1,11 @@
import { Component, createRef, h } from "https://esm.sh/preact@10.17.1";
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";
@ -25,7 +32,7 @@ import { Parsed_Event } from "../nostr.ts";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
import { robohash } from "./relay-detail.tsx";
interface MessageListProps {
interface Props {
myPublicKey: PublicKey;
messages: ChatMessage[];
emit: emitFunc<DirectMessagePanelUpdate | SelectConversation | SyncEvent>;
@ -42,7 +49,7 @@ interface MessageListState {
const ItemsOfPerPage = 50;
export class MessageList extends Component<MessageListProps, MessageListState> {
export class MessageList extends Component<Props, MessageListState> {
readonly messagesULElement = createRef<HTMLUListElement>();
state = {
@ -51,7 +58,7 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
jitter = new JitterPrevention(100);
async componentDidUpdate(previousProps: Readonly<MessageListProps>) {
async componentDidUpdate(previousProps: Readonly<Props>) {
const newest = last(this.props.messages);
const pre_newest = last(previousProps.messages);
if (
@ -74,12 +81,12 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
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 is_not_reply = cur.event.parsedTags.e.length === 0; // todo: make a isReply(event) function
return sameAuthor && _66sec && is_not_reply;
});
const messageBoxGroups = [];
for (const messages of groups) {
const profileEvent = this.props.getters.profileGetter
.getProfilesByPublicKey(messages[0].author);
const profileEvent = this.props.getters.profileGetter.getProfilesByPublicKey(messages[0].author);
messageBoxGroups.push(
MessageBoxGroup({
messages: messages,
@ -92,35 +99,14 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
}
return (
<div
class={`w-full overflow-hidden ${BackgroundColor_MessagePanel}`}
style={{
transform: "perspective(none)",
}}
>
<button
onClick={() => this.goToButtom(true)}
class={`${IconButtonClass} fixed z-10 bottom-8 right-4 h-10 w-10 rotate-[-90deg] bg-[#42464D] hover:bg-[#2F3136]`}
>
<LeftArrowIcon
class={`w-6 h-6`}
style={{
fill: "#F3F4EA",
}}
/>
<div class="w-full flex flex-col overflow-auto">
<button class={`${IconButtonClass} shrink-0`} onClick={this.prePage}>
load earlier messages
</button>
{MessageListView(this.goToButtom, this.messagesULElement, messageBoxGroups)}
<button class={`${IconButtonClass} shrink-0`} onClick={this.nextPage}>
load more messages
</button>
<ul
class={`w-full h-full overflow-y-auto overflow-x-hidden py-9 mobile:py-2 px-2 mobile:px-0 flex flex-col`}
ref={this.messagesULElement}
>
<button class={`${IconButtonClass}`} onClick={this.prePage}>
load earlier messages
</button>
{messageBoxGroups}
<button class={`${IconButtonClass}`} onClick={this.nextPage}>
load more messages
</button>
</ul>
</div>
);
}
@ -170,7 +156,40 @@ export class MessageList extends Component<MessageListProps, MessageListState> {
};
}
export class MessageList_V0 extends Component<MessageListProps> {
function MessageListView(
goToButtom: (smooth: boolean) => void,
messagesULElement: RefObject<HTMLUListElement>,
messageBoxGroups: ComponentChildren,
) {
return (
<div
class={`w-full overflow-hidden ${BackgroundColor_MessagePanel}`}
style={{
transform: "perspective(none)",
}}
>
<button
onClick={() => goToButtom(true)}
class={`${IconButtonClass} fixed z-10 bottom-8 right-4 h-10 w-10 rotate-[-90deg] bg-[#42464D] hover:bg-[#2F3136]`}
>
<LeftArrowIcon
class={`w-6 h-6`}
style={{
fill: "#F3F4EA",
}}
/>
</button>
<ul
class={`w-full h-full overflow-y-auto overflow-x-hidden py-9 mobile:py-2 px-2 mobile:px-0 flex flex-col`}
ref={messagesULElement}
>
{messageBoxGroups}
</ul>
</div>
);
}
export class MessageList_V0 extends Component<Props> {
readonly messagesULElement = createRef<HTMLUListElement>();
jitter = new JitterPrevention(100);
@ -179,7 +198,7 @@ export class MessageList_V0 extends Component<MessageListProps> {
this.goToButtom(false);
}
componentDidUpdate(previousProps: Readonly<MessageListProps>): void {
componentDidUpdate(previousProps: Readonly<Props>): 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
@ -190,7 +209,6 @@ export class MessageList_V0 extends Component<MessageListProps> {
render() {
const messages_to_render = this.sortAndSliceMessage();
console.log(messages_to_render);
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()) <
@ -212,33 +230,7 @@ export class MessageList_V0 extends Component<MessageListProps> {
);
}
return (
<div
class={`w-full overflow-hidden ${BackgroundColor_MessagePanel}`}
style={{
transform: "perspective(none)",
}}
>
<button
id="go to bottom"
onClick={() => this.goToButtom(true)}
class={`${IconButtonClass} fixed z-10 bottom-8 right-4 h-10 w-10 rotate-[-90deg] bg-[#42464D] hover:bg-[#2F3136]`}
>
<LeftArrowIcon
class={`w-6 h-6`}
style={{
fill: "#F3F4EA",
}}
/>
</button>
<ul
class={`w-full h-full overflow-y-auto overflow-x-hidden py-9 mobile:py-2 px-2 mobile:px-0 flex flex-col`}
ref={this.messagesULElement}
>
{messageBoxGroups}
</ul>
</div>
);
return MessageListView(this.goToButtom, this.messagesULElement, messageBoxGroups);
}
sortAndSliceMessage = () => {
@ -292,46 +284,72 @@ function MessageBoxGroup(props: {
}) {
const first_message = props.messages[0];
const rows = [];
// check if the first message is a reply message
function isReply(event: Parsed_Event) {
if (event.parsedTags.e.length == 0) {
return;
}
const reply_to_event = props.getters.getEventByID(event.parsedTags.e[0]);
if (!reply_to_event) {
return <ReplyTo unknown noteId={NoteID.FromString(event.parsedTags.e[0])} />;
}
let author = reply_to_event.publicKey.bech32();
let picture = robohash(reply_to_event.publicKey.hex);
if (reply_to_event.pubkey) {
const profile = props.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 <ReplyTo content={reply_to_event.content} replyName={author} replayPic={picture} />;
}
rows.push(
<li
class={`px-4 hover:bg-[#32353B] w-full max-w-full flex items-start pr-8 mobile:pr-4 group relative ${
class={`px-4 hover:bg-[#32353B] w-full max-w-full flex flex-col pr-8 mobile:pr-4 group relative ${
isMobile() ? "select-none" : ""
}`}
>
{MessageActions(first_message, props.emit)}
<Avatar
class={`h-8 w-8 mt-[0.45rem] mr-2`}
picture={props.authorProfile?.picture ||
robohash(first_message.author.hex)}
onClick={() => {
props.emit({
type: "ViewUserDetail",
pubkey: first_message.author,
});
}}
/>
{isReply(first_message.event)}
<div class="flex items-start">
<Avatar
class={`h-8 w-8 mt-[0.45rem] mr-2`}
picture={props.authorProfile?.picture ||
robohash(first_message.author.hex)}
onClick={() => {
props.emit({
type: "ViewUserDetail",
pubkey: first_message.author,
});
}}
/>
<div
class={`flex-1`}
style={{
maxWidth: "calc(100% - 2.75rem)",
}}
>
{NameAndTime(
first_message.author,
props.authorProfile,
props.myPublicKey,
first_message.created_at,
)}
<pre
class={`text-[#DCDDDE] whitespace-pre-wrap break-words font-roboto text-sm`}
<div
class={`flex-1`}
style={{
maxWidth: "calc(100% - 2.75rem)",
}}
>
{ParseMessageContent(
first_message,
props.emit,
props.getters,
)}
</pre>
{NameAndTime(
first_message.author,
props.authorProfile,
props.myPublicKey,
first_message.created_at,
)}
<pre
class={`text-[#DCDDDE] whitespace-pre-wrap break-words font-roboto text-sm`}
>
{ParseMessageContent(
first_message,
props.emit,
props.getters,
)}
</pre>
</div>
</div>
</li>,
);
@ -368,7 +386,6 @@ function MessageBoxGroup(props: {
</ul>
);
// console.log("MessageBoxGroup", Date.now() - t);
return vnode;
}
@ -410,3 +427,37 @@ function last<T>(array: Array<T>): T | undefined {
return array[array.length - 1];
}
}
function ReplyTo(
props: { unknown?: false; content: string; replyName: string; replayPic: string } | {
unknown: true;
noteId: NoteID;
},
) {
return (
<div class="w-full flex flex-row">
<div class="w-10 h-5 shrink-0">
<div class="w-5 h-2.5 border-l-2 border-t-2 rounded-tl translate-y-2.5 translate-x-4 border-[#4F5058]" />
</div>
<div class="flex flex-row w-full justify-start items-center text-[#A3A6AA] gap-2 font-roboto text-sm pr-5">
{props.unknown
? (
<div class="overflow-hidden whitespace-nowrap text-overflow-ellipsis">
{props.noteId.bech32()}
</div>
)
: (
<>
<Avatar class="h-4 w-4 shrink-0" picture={props.replayPic || ""} />
<div class="whitespace-nowrap md:shrink-0 truncate w-30">
@{props.replyName}
</div>
<div class="overflow-hidden whitespace-nowrap truncate text-overflow-ellipsis w-[90%]">
{props.content}
</div>
</>
)}
</div>
</div>
);
}