Chat system refactor
This commit is contained in:
@ -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>
|
||||
);
|
||||
|
@ -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) {
|
||||
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
</>
|
Reference in New Issue
Block a user