Loading packages/chat/lib/components/ChatLog/ChatBubble.tsx +80 −36 Original line number Diff line number Diff line import React, { useState, useEffect } from "react"; import { useYap } from "@/context/utils"; import type { MatrixEvent } from "matrix-js-sdk"; import * as sdk from "matrix-js-sdk"; import { MessageBody } from "./MessageBody"; import { ReadReceipts } from "./ReadReceipts"; export const ChatBubble = ({ event }: { event: MatrixEvent }) => { export const ChatBubble = React.memo( ({ room, event, isLatest, }: { room: sdk.Room; event: sdk.MatrixEvent; isLatest: boolean; }) => { const { client } = useYap<"Internal">(); const isRight = event.getSender() === client.client.getUserId(); const [edited, setEdited] = useState(!!event.replacingEvent()); const [content, setContent] = useState<{ body?: string; [key: string]: unknown; } | null>(event.getContent()); useEffect(() => { const update = () => { setContent(event.getContent()); setEdited(!!event.replacingEvent()); }; event.on(sdk.MatrixEventEvent.Decrypted, update); event.on(sdk.MatrixEventEvent.Replaced, update); return () => { event.off(sdk.MatrixEventEvent.Decrypted, update); event.off(sdk.MatrixEventEvent.Replaced, update); }; }, [event]); return ( <div Loading @@ -17,25 +49,37 @@ export const ChatBubble = ({ event }: { event: MatrixEvent }) => { ) : ( */} <> <div className="flex flex-row gap-1 *:text-xs"> <span>{event.getSender()}</span> <span> {event.getSender() && room.getMember(event.getSender()!)?.name} </span> </div> <div className="rounded-xl p-1" className="rounded-xl p-1 max-w-full" style={{ backgroundColor: isRight ? "var(--color-gray-500)" : "var(--color-gray-300)", }} > {event.getContent().body} <p style={{ wordBreak: "break-word", whiteSpace: "normal", }} > <MessageBody event={event} /> </p> </div> <div className="flex flex-row gap-1 *:text-xs"> {/* {flags & MessageFlag.SENDING ? <span>sending</span> : ""} {flags & MessageFlag.SEEN ? <span>seen</span> : ""} {flags & MessageFlag.EDITED ? <span>edited</span> : ""} */} {edited && <span>edited</span>} {event.isSending() && <span>sending...</span>} {isLatest && ( <ReadReceipts client={client} room={room} event={event} /> )} </div> </> {/* )} */} </div> ); }; }, ); packages/chat/lib/components/ChatLog/ChatTimeline.tsx +46 −56 Original line number Diff line number Diff line import { MXClient } from "@/lib/MXClient"; import type { MXRoomID } from "@/main"; import { ClientEvent, EventTimeline, MatrixEvent, RoomEvent, SyncState, TimelineWindow, } from "matrix-js-sdk"; import { useEffect, useRef, useState } from "react"; import * as sdk from "matrix-js-sdk"; import { useCallback, useEffect, useRef, useState } from "react"; import { ChatBubble } from "./ChatBubble"; import { useTimeline } from "@/hooks/useTimeline"; export const ChatTimeline = ({ roomId }: { roomId: MXRoomID }) => { const timelineRef = useRef<TimelineWindow>(null); const [events, setEvents] = useState<MatrixEvent[]>([]); function getTimeline() { if (timelineRef.current !== null) { return timelineRef.current; } const { client, rooms } = MXClient.get(); const room = rooms.getRoom(roomId); return (timelineRef.current = new TimelineWindow( export const ChatTimeline = ({ client, room!.getUnfilteredTimelineSet()!, )); } room, }: { client: MXClient; room: sdk.Room; }) => { const bottom = useRef<HTMLLIElement>(null); const { events, forward, backward, loading } = useTimeline(client, room); const scrollBottom = useCallback(() => { bottom.current?.scrollIntoView({ behavior: "smooth" }); }, []); useEffect(() => { const { client } = MXClient.get(); getTimeline() .load() .then(() => { setEvents(getTimeline().getEvents()); }) .catch((e) => { console.error(e); }); scrollBottom(); }, [events, scrollBottom]); client.on(RoomEvent.Timeline, (event, room, toStartOfTimeline) => { if (!room || room.roomId !== roomId) return; if (toStartOfTimeline) { console.log("tostartoftimeline"); return; } getTimeline() .paginate(EventTimeline.FORWARDS, 1, false) .then(() => { setEvents(getTimeline().getEvents()); }); }); }, [roomId]); useEffect(() => { bottom.current?.scrollIntoView({ behavior: "instant" }); }, []); return ( <ul> {events.map((ev) => ( <li key={ev.getId()}> <ChatBubble event={ev} /> <li onClick={() => backward()}>top {loading.backward && "loading"}</li> {events .filter( (ev) => !ev.isRelation("m.replace") && !ev.isRelation("m.annotation") && !ev.isRedaction() && !ev.isRedacted(), ) .map((ev, i, arr) => ( <li key={ev.getId()} onClick={() => console.log(ev)}> {ev.getType()} {ev.getType() === "m.room.message" && ( <ChatBubble room={room} event={ev} isLatest={i === arr.length - 1} /> )} </li> ))} <li onClick={() => forward()} ref={bottom}> bottom {loading.forward && "loading"} </li> </ul> ); }; packages/chat/lib/components/ChatLog/MessageBody.tsx 0 → 100644 +21 −0 Original line number Diff line number Diff line import React, { useEffect, useMemo, useState } from "react"; import * as sdk from "matrix-js-sdk"; import DOMPurify from "dompurify"; export const MessageBody = ({ event }: { event: sdk.MatrixEvent }) => { const content = event.getContent(); const rendered = useMemo(() => { if (content.format === "org.matrix.custom.html" && content.formatted_body) { const html = DOMPurify.sanitize(content.formatted_body, { ADD_FORBID_CONTENTS: ["mx-reply"], }); return <span dangerouslySetInnerHTML={{ __html: html }} />; } return <span>{content.body}</span>; }, [content]); return <>{rendered}</>; }; packages/chat/lib/components/ChatLog/ReadReceipts.tsx 0 → 100644 +17 −0 Original line number Diff line number Diff line import { useRead } from "@/hooks/useRead"; import type { MXClient } from "@/lib/MXClient"; import * as sdk from "matrix-js-sdk"; export const ReadReceipts = ({ client, room, event, }: { client: MXClient; room: sdk.Room; event: sdk.MatrixEvent; }) => { const { readBy } = useRead(client, room, event); return <>Read ({readBy.length})</>; }; packages/chat/lib/components/Tab/Room.tsx +6 −3 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { MXClient } from "@/lib/MXClient"; import { Room } from "matrix-js-sdk"; import { ChatTimeline } from "../ChatLog/ChatTimeline"; import { useTimeline } from "@/hooks/useTimeline"; export const RoomTab = ({ roomId, Loading @@ -19,7 +20,6 @@ export const RoomTab = ({ const { dispatch } = useYap<"Internal">(); const room = useRoom(roomId); const [message, setMessage] = useState(""); const { messages, loading, atBeginning, loadMore } = useMessages(roomId); const sendMessage = useCallback( (e: React.SubmitEvent) => { Loading @@ -44,6 +44,7 @@ export const RoomTab = ({ name={room.name || `room: ${roomId}`} isNagging={isNagging} className="flex flex-col" defaultOpen onClose={() => dispatch(["closeChat", roomId])} onFocus={() => dispatch(["unnag", roomId])} > Loading @@ -55,7 +56,7 @@ export const RoomTab = ({ )} {room.membership === "join" && ( <> <div className="p-2 grow overflow-y-auto"> <div className="p-2 grow overflow-y-auto overflow-x-hidden"> {/* <ChatBubble role="ME" flags={MessageFlag.SENDING ^ MessageFlag.SEEN} /> <ChatBubble role="SYSTEM" /> <ChatBubble role="OTHER" /> */} Loading @@ -65,7 +66,9 @@ export const RoomTab = ({ atBeginning={atBeginning} loadMore={loadMore} /> */} <ChatTimeline roomId={roomId} /> {room.room && ( <ChatTimeline client={MXClient.get()} room={room.room} /> )} </div> {room.replacedBy ? ( <ReplacedByBanner roomId={roomId} /> Loading Loading
packages/chat/lib/components/ChatLog/ChatBubble.tsx +80 −36 Original line number Diff line number Diff line import React, { useState, useEffect } from "react"; import { useYap } from "@/context/utils"; import type { MatrixEvent } from "matrix-js-sdk"; import * as sdk from "matrix-js-sdk"; import { MessageBody } from "./MessageBody"; import { ReadReceipts } from "./ReadReceipts"; export const ChatBubble = ({ event }: { event: MatrixEvent }) => { export const ChatBubble = React.memo( ({ room, event, isLatest, }: { room: sdk.Room; event: sdk.MatrixEvent; isLatest: boolean; }) => { const { client } = useYap<"Internal">(); const isRight = event.getSender() === client.client.getUserId(); const [edited, setEdited] = useState(!!event.replacingEvent()); const [content, setContent] = useState<{ body?: string; [key: string]: unknown; } | null>(event.getContent()); useEffect(() => { const update = () => { setContent(event.getContent()); setEdited(!!event.replacingEvent()); }; event.on(sdk.MatrixEventEvent.Decrypted, update); event.on(sdk.MatrixEventEvent.Replaced, update); return () => { event.off(sdk.MatrixEventEvent.Decrypted, update); event.off(sdk.MatrixEventEvent.Replaced, update); }; }, [event]); return ( <div Loading @@ -17,25 +49,37 @@ export const ChatBubble = ({ event }: { event: MatrixEvent }) => { ) : ( */} <> <div className="flex flex-row gap-1 *:text-xs"> <span>{event.getSender()}</span> <span> {event.getSender() && room.getMember(event.getSender()!)?.name} </span> </div> <div className="rounded-xl p-1" className="rounded-xl p-1 max-w-full" style={{ backgroundColor: isRight ? "var(--color-gray-500)" : "var(--color-gray-300)", }} > {event.getContent().body} <p style={{ wordBreak: "break-word", whiteSpace: "normal", }} > <MessageBody event={event} /> </p> </div> <div className="flex flex-row gap-1 *:text-xs"> {/* {flags & MessageFlag.SENDING ? <span>sending</span> : ""} {flags & MessageFlag.SEEN ? <span>seen</span> : ""} {flags & MessageFlag.EDITED ? <span>edited</span> : ""} */} {edited && <span>edited</span>} {event.isSending() && <span>sending...</span>} {isLatest && ( <ReadReceipts client={client} room={room} event={event} /> )} </div> </> {/* )} */} </div> ); }; }, );
packages/chat/lib/components/ChatLog/ChatTimeline.tsx +46 −56 Original line number Diff line number Diff line import { MXClient } from "@/lib/MXClient"; import type { MXRoomID } from "@/main"; import { ClientEvent, EventTimeline, MatrixEvent, RoomEvent, SyncState, TimelineWindow, } from "matrix-js-sdk"; import { useEffect, useRef, useState } from "react"; import * as sdk from "matrix-js-sdk"; import { useCallback, useEffect, useRef, useState } from "react"; import { ChatBubble } from "./ChatBubble"; import { useTimeline } from "@/hooks/useTimeline"; export const ChatTimeline = ({ roomId }: { roomId: MXRoomID }) => { const timelineRef = useRef<TimelineWindow>(null); const [events, setEvents] = useState<MatrixEvent[]>([]); function getTimeline() { if (timelineRef.current !== null) { return timelineRef.current; } const { client, rooms } = MXClient.get(); const room = rooms.getRoom(roomId); return (timelineRef.current = new TimelineWindow( export const ChatTimeline = ({ client, room!.getUnfilteredTimelineSet()!, )); } room, }: { client: MXClient; room: sdk.Room; }) => { const bottom = useRef<HTMLLIElement>(null); const { events, forward, backward, loading } = useTimeline(client, room); const scrollBottom = useCallback(() => { bottom.current?.scrollIntoView({ behavior: "smooth" }); }, []); useEffect(() => { const { client } = MXClient.get(); getTimeline() .load() .then(() => { setEvents(getTimeline().getEvents()); }) .catch((e) => { console.error(e); }); scrollBottom(); }, [events, scrollBottom]); client.on(RoomEvent.Timeline, (event, room, toStartOfTimeline) => { if (!room || room.roomId !== roomId) return; if (toStartOfTimeline) { console.log("tostartoftimeline"); return; } getTimeline() .paginate(EventTimeline.FORWARDS, 1, false) .then(() => { setEvents(getTimeline().getEvents()); }); }); }, [roomId]); useEffect(() => { bottom.current?.scrollIntoView({ behavior: "instant" }); }, []); return ( <ul> {events.map((ev) => ( <li key={ev.getId()}> <ChatBubble event={ev} /> <li onClick={() => backward()}>top {loading.backward && "loading"}</li> {events .filter( (ev) => !ev.isRelation("m.replace") && !ev.isRelation("m.annotation") && !ev.isRedaction() && !ev.isRedacted(), ) .map((ev, i, arr) => ( <li key={ev.getId()} onClick={() => console.log(ev)}> {ev.getType()} {ev.getType() === "m.room.message" && ( <ChatBubble room={room} event={ev} isLatest={i === arr.length - 1} /> )} </li> ))} <li onClick={() => forward()} ref={bottom}> bottom {loading.forward && "loading"} </li> </ul> ); };
packages/chat/lib/components/ChatLog/MessageBody.tsx 0 → 100644 +21 −0 Original line number Diff line number Diff line import React, { useEffect, useMemo, useState } from "react"; import * as sdk from "matrix-js-sdk"; import DOMPurify from "dompurify"; export const MessageBody = ({ event }: { event: sdk.MatrixEvent }) => { const content = event.getContent(); const rendered = useMemo(() => { if (content.format === "org.matrix.custom.html" && content.formatted_body) { const html = DOMPurify.sanitize(content.formatted_body, { ADD_FORBID_CONTENTS: ["mx-reply"], }); return <span dangerouslySetInnerHTML={{ __html: html }} />; } return <span>{content.body}</span>; }, [content]); return <>{rendered}</>; };
packages/chat/lib/components/ChatLog/ReadReceipts.tsx 0 → 100644 +17 −0 Original line number Diff line number Diff line import { useRead } from "@/hooks/useRead"; import type { MXClient } from "@/lib/MXClient"; import * as sdk from "matrix-js-sdk"; export const ReadReceipts = ({ client, room, event, }: { client: MXClient; room: sdk.Room; event: sdk.MatrixEvent; }) => { const { readBy } = useRead(client, room, event); return <>Read ({readBy.length})</>; };
packages/chat/lib/components/Tab/Room.tsx +6 −3 Original line number Diff line number Diff line Loading @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { MXClient } from "@/lib/MXClient"; import { Room } from "matrix-js-sdk"; import { ChatTimeline } from "../ChatLog/ChatTimeline"; import { useTimeline } from "@/hooks/useTimeline"; export const RoomTab = ({ roomId, Loading @@ -19,7 +20,6 @@ export const RoomTab = ({ const { dispatch } = useYap<"Internal">(); const room = useRoom(roomId); const [message, setMessage] = useState(""); const { messages, loading, atBeginning, loadMore } = useMessages(roomId); const sendMessage = useCallback( (e: React.SubmitEvent) => { Loading @@ -44,6 +44,7 @@ export const RoomTab = ({ name={room.name || `room: ${roomId}`} isNagging={isNagging} className="flex flex-col" defaultOpen onClose={() => dispatch(["closeChat", roomId])} onFocus={() => dispatch(["unnag", roomId])} > Loading @@ -55,7 +56,7 @@ export const RoomTab = ({ )} {room.membership === "join" && ( <> <div className="p-2 grow overflow-y-auto"> <div className="p-2 grow overflow-y-auto overflow-x-hidden"> {/* <ChatBubble role="ME" flags={MessageFlag.SENDING ^ MessageFlag.SEEN} /> <ChatBubble role="SYSTEM" /> <ChatBubble role="OTHER" /> */} Loading @@ -65,7 +66,9 @@ export const RoomTab = ({ atBeginning={atBeginning} loadMore={loadMore} /> */} <ChatTimeline roomId={roomId} /> {room.room && ( <ChatTimeline client={MXClient.get()} room={room.room} /> )} </div> {room.replacedBy ? ( <ReplacedByBanner roomId={roomId} /> Loading