/** @jsx h */
import { Component, ComponentChildren, createRef, Fragment, h, VNode } from "https://esm.sh/preact@10.11.3";
import { tw } from "https://esm.sh/twind@0.16.16";
import { Editor, EditorEvent, EditorModel } from "./editor.tsx";
import { 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 { EventEmitter } from "../event-bus.ts";
import {
ChatMessage,
groupContinuousMessages,
isImage,
parseContent,
sortMessage,
urlIsImage,
} from "./message.ts";
import { PublicKey } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts";
import {
NostrEvent,
NostrKind,
} from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts";
import { PinContact, UnpinContact } from "../nostr.ts";
import { ProfileData } from "../features/profile.ts";
import { MessageThread } from "./dm.tsx";
import { UserDetail } from "./user-detail.tsx";
import { MessageThreadPanel } from "./message-thread-panel.tsx";
interface DirectMessagePanelProps {
myPublicKey: PublicKey;
editorModel: EditorModel;
messages: MessageThread[];
focusedContent: {
type: "MessageThread";
data: MessageThread;
editor: EditorModel;
} | {
type: "ProfileData";
data?: ProfileData;
pubkey: PublicKey;
} | undefined;
rightPanelModel: RightPanelModel;
eventEmitter: EventEmitter<
EditorEvent | DirectMessagePanelUpdate | PinContact | UnpinContact
>;
}
export type RightPanelModel = {
show: boolean;
};
export type DirectMessagePanelUpdate =
| {
type: "ToggleRightPanel";
show: boolean;
}
| ViewThread
| ViewUserDetail;
export type ViewThread = {
type: "ViewThread";
root: NostrEvent;
};
export type ViewUserDetail = {
type: "ViewUserDetail";
pubkey: PublicKey;
};
export function MessagePanel(props: DirectMessagePanelProps) {
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[];
eventEmitter?: EventEmitter;
}
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 = [];
for (const threads of groups) {
messageBoxGroups.push(
MessageBoxGroup({
messageGroup: threads.map((thread) => {
return {
msg: thread.root,
replyCount: thread.replies.length,
};
}),
myPublicKey: this.props.myPublicKey,
eventEmitter: this.props.eventEmitter,
}),
);
}
console.log("MessageList:elements", Date.now() - t);
const vNode = (
);
console.log("MessageList:end", Date.now() - t);
return vNode;
}
}
function MessageBoxGroup(props: {
messageGroup: {
msg: ChatMessage;
replyCount: number;
}[];
myPublicKey: PublicKey;
eventEmitter?: EventEmitter;
}) {
// const t = Date.now();
const vnode = (
{props.messageGroup.reverse().map((msg, index) => {
return (
-
{AvatarOrTime(msg.msg, index, props.eventEmitter)}
{NameAndTime(msg.msg, index, props.myPublicKey)}
{ParseMessageContent(msg.msg)}
{msg.replyCount > 0
? (
{
props.eventEmitter?.emit({
type: "ViewThread",
root: msg.msg.event,
});
}}
>
{msg.replyCount} replies
)
: undefined}
);
})}
);
// console.log("MessageBoxGroup", Date.now() - t);
return vnode;
}
export function AvatarOrTime(
message: ChatMessage,
index: number,
eventEmitter?: EventEmitter,
) {
if (index === 0) {
return (
{
eventEmitter.emit({
type: "ViewUserDetail",
pubkey: message.author.pubkey,
});
}
: undefined}
/>
);
}
return (
{message.created_at.toTimeString().slice(0, 5)}
);
}
export function NameAndTime(message: ChatMessage, index: number, myPublicKey: PublicKey) {
if (index === 0) {
return (
{message.author
? (
message.author.pubkey.hex ===
myPublicKey.hex
? "Me"
: message.author.name ||
message.author.pubkey.bech32()
)
: "no user meta"}
{message.created_at.toLocaleString()}
);
}
}
export function ParseMessageContent(message: ChatMessage) {
let vnode: VNode | VNode[];
if (isImage(message)) {
vnode = ;
} else {
const items = Array.from(parseContent(message.content));
vnode = [{message.content}
];
for (const item of items) {
switch (item.type) {
case "url":
const url = message.content.slice(item.start, item.end + 1);
if (urlIsImage(url)) {
vnode.push();
}
break;
case "npub":
// todo
break;
case "tag":
// todo
break;
}
}
}
return vnode;
}
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;
}
}