Commit 3991b268 authored by Grant's avatar Grant
Browse files

more work:

- dompurify
- timeline syncing
- pagination of timeline
- initial read receipts
parent 9d5cfe22
Loading
Loading
Loading
Loading
Loading
+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
@@ -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>
    );
};
  },
);
+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>
  );
};
+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}</>;
};
+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})</>;
};
+6 −3
Original line number Diff line number Diff line
@@ -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,
@@ -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) => {
@@ -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])}
    >
@@ -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" /> */}
@@ -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