Chat system refactor

This commit is contained in:
2023-06-20 14:15:33 +01:00
parent 4365fac9b5
commit 234c1c092d
22 changed files with 397 additions and 263 deletions

View File

@ -2,55 +2,65 @@ import "./DM.css";
import { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { useInView } from "react-intersection-observer";
import { TaggedRawEvent } from "@snort/system";
import { EventKind, TaggedRawEvent } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import NoteTime from "Element/NoteTime";
import Text from "Element/Text";
import { setLastReadDm } from "Pages/MessagesPage";
import { unwrap } from "SnortUtils";
import useLogin from "Hooks/useLogin";
import { Chat, ChatType, chatTo, setLastReadIn } from "chat";
import messages from "./messages";
import ProfileImage from "./ProfileImage";
export type DMProps = {
export interface DMProps {
chat: Chat;
data: TaggedRawEvent;
};
}
export default function DM(props: DMProps) {
const pubKey = useLogin().publicKey;
const publisher = useEventPublisher();
const [content, setContent] = useState("Loading...");
const ev = props.data;
const needsDecryption = ev.kind === EventKind.DirectMessage;
const [content, setContent] = useState(needsDecryption ? "Loading..." : ev.content);
const [decrypted, setDecrypted] = useState(false);
const { ref, inView } = useInView();
const { formatMessage } = useIntl();
const isMe = props.data.pubkey === pubKey;
const otherPubkey = isMe ? pubKey : unwrap(props.data.tags.find(a => a[0] === "p")?.[1]);
const isMe = ev.pubkey === pubKey;
const otherPubkey = isMe ? pubKey : chatTo(ev);
async function decrypt() {
if (publisher) {
const decrypted = await publisher.decryptDm(props.data);
const decrypted = await publisher.decryptDm(ev);
setContent(decrypted || "<ERROR>");
if (!isMe) {
setLastReadDm(props.data.pubkey);
setLastReadIn(ev.pubkey);
}
}
}
function sender() {
if (props.chat.type !== ChatType.DirectMessage && !isMe) {
return <ProfileImage pubkey={ev.pubkey} />;
}
}
useEffect(() => {
if (!decrypted && inView) {
if (!decrypted && inView && needsDecryption) {
setDecrypted(true);
decrypt().catch(console.error);
}
}, [inView, props.data]);
}, [inView, ev]);
return (
<div className={isMe ? "dm me" : "dm other"} ref={ref}>
<div>
{sender()}
<Text content={content} tags={[]} creator={otherPubkey} />
</div>
<div>
<NoteTime from={props.data.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
<NoteTime from={ev.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
</div>
</div>
);

View File

@ -8,6 +8,8 @@
overflow-y: auto;
padding: 0 10px 10px 10px;
flex-grow: 1;
display: flex;
flex-direction: column-reverse;
}
.dm-window > div:nth-child(3) {

View File

@ -1,85 +1,60 @@
import "./DmWindow.css";
import { useEffect, useMemo, useRef } from "react";
import { useMemo } from "react";
import { TaggedRawEvent } from "@snort/system";
import ProfileImage from "Element/ProfileImage";
import DM from "Element/DM";
import { dmsForLogin, dmsInChat, isToSelf } from "Pages/MessagesPage";
import NoteToSelf from "Element/NoteToSelf";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
import WriteDm from "Element/WriteDm";
import { unwrap } from "SnortUtils";
import WriteMessage from "Element/WriteMessage";
import { Chat, ChatType, useChatSystem } from "chat";
export default function DmWindow({ id }: { id: string }) {
const pubKey = useLogin().publicKey;
const dmListRef = useRef<HTMLDivElement>(null);
const dms = useChatSystem();
const chat = dms.find(a => a.id === id);
function resize(chatList: HTMLDivElement) {
if (!chatList.parentElement) return;
const scrollWrap = unwrap(chatList.parentElement);
const h = scrollWrap.scrollHeight;
const s = scrollWrap.clientHeight + scrollWrap.scrollTop;
const pos = Math.abs(h - s);
const atBottom = pos === 0;
//console.debug("Resize", h, s, pos, atBottom);
if (atBottom) {
scrollWrap.scrollTo(0, scrollWrap.scrollHeight);
function sender() {
if (id === pubKey) {
return <NoteToSelf className="f-grow mb-10" pubkey={id} />;
}
if (chat?.type === ChatType.DirectMessage) {
return <ProfileImage pubkey={id} className="f-grow mb10" />;
}
if (chat?.profile) {
return <ProfileImage pubkey={id} className="f-grow mb10" profile={chat.profile} />;
}
return <ProfileImage pubkey={""} className="f-grow mb10" overrideUsername={chat?.id} />;
}
useEffect(() => {
if (dmListRef.current) {
const scrollWrap = dmListRef.current;
const chatList = unwrap(scrollWrap.parentElement);
chatList.onscroll = () => {
resize(dmListRef.current as HTMLDivElement);
};
new ResizeObserver(() => resize(dmListRef.current as HTMLDivElement)).observe(scrollWrap);
return () => {
chatList.onscroll = null;
new ResizeObserver(() => resize(dmListRef.current as HTMLDivElement)).unobserve(scrollWrap);
};
}
}, [dmListRef]);
return (
<div className="dm-window">
<div>{sender()}</div>
<div>
{(id === pubKey && <NoteToSelf className="f-grow mb-10" pubkey={id} />) || (
<ProfileImage pubkey={id} className="f-grow mb10" />
)}
<div className="flex f-col">{chat && <DmChatSelected chat={chat} />}</div>
</div>
<div>
<div className="flex f-col" ref={dmListRef}>
<DmChatSelected chatPubKey={id} />
</div>
</div>
<div>
<WriteDm chatPubKey={id} />
<WriteMessage chatId={id} />
</div>
</div>
);
}
function DmChatSelected({ chatPubKey }: { chatPubKey: string }) {
const dms = useDmCache();
function DmChatSelected({ chat }: { chat: Chat }) {
const { publicKey: myPubKey } = useLogin();
const sortedDms = useMemo(() => {
if (myPubKey) {
const myDms = dmsForLogin(dms, myPubKey);
const myDms = chat?.messages;
if (myPubKey && myDms) {
// filter dms in this chat, or dms to self
const thisDms = myPubKey === chatPubKey ? myDms.filter(d => isToSelf(d, myPubKey)) : myDms;
return [...dmsInChat(thisDms, chatPubKey)].sort((a, b) => a.created_at - b.created_at);
return [...myDms].sort((a, b) => a.created_at - b.created_at);
}
return [];
}, [dms, myPubKey, chatPubKey]);
}, [chat, myPubKey]);
return (
<>
{sortedDms.map(a => (
<DM data={a as TaggedRawEvent} key={a.id} />
<DM data={a as TaggedRawEvent} key={a.id} chat={chat} />
))}
</>
);

View File

@ -2,7 +2,7 @@ import "./ProfileImage.css";
import React, { useMemo } from "react";
import { Link } from "react-router-dom";
import { HexKey, NostrPrefix, MetadataCache } from "@snort/system";
import { HexKey, NostrPrefix, UserMetadata } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { hexToBech32, profileLink } from "SnortUtils";
@ -19,6 +19,7 @@ export interface ProfileImageProps {
defaultNip?: string;
verifyNip?: boolean;
overrideUsername?: string;
profile?: UserMetadata;
}
export default function ProfileImage({
@ -30,8 +31,9 @@ export default function ProfileImage({
defaultNip,
verifyNip,
overrideUsername,
profile,
}: ProfileImageProps) {
const user = useUserProfile(System, pubkey);
const user = profile ?? useUserProfile(System, pubkey);
const nip05 = defaultNip ? defaultNip : user?.nip05;
const name = useMemo(() => {
@ -66,7 +68,7 @@ export default function ProfileImage({
);
}
export function getDisplayName(user: MetadataCache | undefined, pubkey: HexKey) {
export function getDisplayName(user: UserMetadata | undefined, pubkey: HexKey) {
let name = hexToBech32(NostrPrefix.PublicKey, pubkey).substring(0, 12);
if (typeof user?.display_name === "string" && user.display_name.length > 0) {
name = user.display_name;

View File

@ -7,8 +7,9 @@ import useFileUpload from "Upload";
import { openFile } from "SnortUtils";
import Textarea from "./Textarea";
import { System } from "index";
import { useChatSystem } from "chat";
export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
export default function WriteMessage({ chatId }: { chatId: string }) {
const [msg, setMsg] = useState("");
const [sending, setSending] = useState(false);
const [uploading, setUploading] = useState(false);
@ -16,6 +17,7 @@ export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
const [error, setError] = useState("");
const publisher = useEventPublisher();
const uploader = useFileUpload();
const chat = useChatSystem().find(a => a.id === chatId);
async function attachFile() {
try {
@ -54,11 +56,11 @@ export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
}
}
async function sendDm() {
if (msg && publisher) {
async function sendMessage() {
if (msg && publisher && chat) {
setSending(true);
const ev = await publisher.sendDm(msg, chatPubKey);
System.BroadcastEvent(ev);
const ev = await chat.createMessage(msg, publisher);
await chat.sendMessage(ev, System);
setMsg("");
setSending(false);
}
@ -73,7 +75,8 @@ export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
async function onEnter(e: React.KeyboardEvent<HTMLTextAreaElement>) {
const isEnter = e.code === "Enter";
if (isEnter && !e.shiftKey) {
await sendDm();
e.preventDefault();
await sendMessage();
}
}
@ -96,7 +99,7 @@ export default function WriteDm({ chatPubKey }: { chatPubKey: string }) {
/>
{error && <b className="error">{error}</b>}
</div>
<button className="btn-rnd" onClick={() => sendDm()}>
<button className="btn-rnd" onClick={() => sendMessage()}>
{sending ? <Spinner width={20} /> : <Icon name="arrow-right" size={20} />}
</button>
</>