Commit 1ae29552 authored by Grant's avatar Grant
Browse files

fix read receipts, reactions & load on scroll

parent 3991b268
Loading
Loading
Loading
Loading
Loading
+68 −71
Original line number Diff line number Diff line
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { useYap } from "@/context/utils";
import * as sdk from "matrix-js-sdk";
import { MessageBody } from "./MessageBody";
import { ReadReceipts } from "./ReadReceipts";
import { Reactions } from "./Reactions";

export const ChatBubble = React.memo(
  ({
export const ChatBubble = ({
  room,
  event,
  isLatest,
@@ -17,17 +17,14 @@ export const ChatBubble = React.memo(
  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());
    };

    update();

    event.on(sdk.MatrixEventEvent.Decrypted, update);
    event.on(sdk.MatrixEventEvent.Replaced, update);

@@ -35,7 +32,7 @@ export const ChatBubble = React.memo(
      event.off(sdk.MatrixEventEvent.Decrypted, update);
      event.off(sdk.MatrixEventEvent.Replaced, update);
    };
    }, [event]);
  }, [event, client]);

  return (
    <div
@@ -76,10 +73,10 @@ export const ChatBubble = React.memo(
          {isLatest && (
            <ReadReceipts client={client} room={room} event={event} />
          )}
          <Reactions client={client} event={event} />
        </div>
      </>
      {/* )} */}
    </div>
  );
  },
);
};
+145 −35
Original line number Diff line number Diff line
import { MXClient } from "@/lib/MXClient";
import type { MXRoomID } from "@/main";
import { useOnInView } from "react-intersection-observer";
import type { MXEventID, MXUserID } from "@/main";
import * as sdk from "matrix-js-sdk";
import { useCallback, useEffect, useRef, useState } from "react";
import { createContext, useEffect, useReducer, useRef } from "react";
import { ChatBubble } from "./ChatBubble";
import { useTimeline } from "@/hooks/useTimeline";
import { isVisibleEvent } from "@/lib/isVisibleEvent";

export const RoomContext = createContext<{
  reactions: {
    [eventId: MXEventID]: {
      [emoji: string]: { [userId: MXUserID]: MXEventID };
    };
  };
}>({ reactions: {} });

// input is already known to be an event id, this is for types
const isEventId = (input: string): input is MXEventID => {
  return input.startsWith("$");
};

// input is already known to be an user id, this is for types
const isUserId = (input: string): input is MXUserID => {
  return input.startsWith("@");
};

export const ChatTimeline = ({
  client,
@@ -12,24 +32,113 @@ export const ChatTimeline = ({
  client: MXClient;
  room: sdk.Room;
}) => {
  const top = useOnInView((inView) => {
    if (inView) {
      backward();
    }
  });
  const bottom = useRef<HTMLLIElement>(null);
  const { events, forward, backward, loading } = useTimeline(client, room);

  const scrollBottom = useCallback(() => {
  const { events, forward, backward, loading, more } = useTimeline(
    client,
    room,
    (ev) => {
      if (isVisibleEvent(ev)) {
        bottom.current?.scrollIntoView({ behavior: "smooth" });
  }, []);
      }
    },
  );
  const [reactions, dReactions] = useReducer(
    (prev, action: [events: sdk.MatrixEvent[]]) => {
      const state = { ...prev };

  useEffect(() => {
    scrollBottom();
  }, [events, scrollBottom]);
      for (const event of action[0]) {
        if (event.getType() === sdk.EventType.RoomRedaction) {
          const content = event.getContent();
          if (!("redacts" in content) || typeof content.redacts !== "string")
            continue;
          const eventToRedact = content.redacts as MXEventID;

          for (const event_id in state) {
            for (const emoji in state[event_id as MXEventID]) {
              Object.entries(state[event_id as MXEventID][emoji]).forEach(
                ([userId, reactionEventId]) => {
                  if (reactionEventId === eventToRedact) {
                    delete state[event_id as MXEventID][emoji][userId];

                    if (
                      Object.keys(state[event_id as MXEventID][emoji])
                        .length === 0
                    ) {
                      delete state[event_id as MXEventID][emoji];
                    }
                  }
                },
              );
            }
          }
        } else {
          const emoji = event.getRelation()?.key;
          const event_id = event.getRelation()?.event_id;
          if (!emoji || !event_id || !isEventId(event_id)) return state;
          if (!(event_id in state)) state[event_id] = {};
          if (!(emoji in state[event_id])) state[event_id][emoji] = {};

          const sender = event.getSender();
          if (!sender || !isUserId(sender)) return state;

          state[event_id][emoji][sender] = event.getId()! as MXEventID;
        }
      }

      return state;
    },
    {} as {
      [eventId: MXEventID]: {
        [emoji: string]: { [userId: MXUserID]: MXEventID };
      };
    },
  );

  useEffect(() => {
    bottom.current?.scrollIntoView({ behavior: "instant" });
  }, []);

  useEffect(() => {
    const handleNew = (event: sdk.MatrixEvent) => {
      const relation = event.getRelation();

      if (relation && relation.rel_type === sdk.RelationType.Annotation) {
        dReactions([[event]]);
      }
    };

    const handleRedact = (event: sdk.MatrixEvent) => {
      dReactions([[event]]);
    };

    const events = room
      .getUnfilteredTimelineSet()
      .getLiveTimeline()
      .getEvents();
    for (const event of events) {
      handleNew(event);
    }

    room.on(sdk.RoomEvent.Timeline, handleNew);
    room.on(sdk.RoomEvent.Redaction, handleRedact);

    return () => {
      room.off(sdk.RoomEvent.Timeline, handleNew);
      room.off(sdk.RoomEvent.Redaction, handleRedact);
    };
  }, []);

  return (
    <RoomContext value={{ reactions }}>
      <ul>
      <li onClick={() => backward()}>top {loading.backward && "loading"}</li>
        <li ref={top} onClick={() => backward()}>
          top {loading.backward && "loading"} {more.backward && "has more"}
        </li>
        {events
          .filter(
            (ev) =>
@@ -51,8 +160,9 @@ export const ChatTimeline = ({
            </li>
          ))}
        <li onClick={() => forward()} ref={bottom}>
        bottom {loading.forward && "loading"}
          bottom {loading.forward && "loading"} {more.forward && "has more"}
        </li>
      </ul>
    </RoomContext>
  );
};
+19 −0
Original line number Diff line number Diff line
import type { MXClient } from "@/lib/MXClient";
import type { MXEventID } from "@/main";
import * as sdk from "matrix-js-sdk";
import { use } from "react";
import { RoomContext } from "./ChatTimeline";

export const Reactions = ({
  client,
  event,
}: {
  client: MXClient;
  event: sdk.MatrixEvent;
}) => {
  const { reactions } = use(RoomContext);

  return (
    <>{Object.keys(reactions[event.getId()! as MXEventID] || {}).join(",")}</>
  );
};
+1 −1
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ export const ReadReceipts = ({
  room: sdk.Room;
  event: sdk.MatrixEvent;
}) => {
  const { readBy } = useRead(client, room, event);
  const readBy = useRead(client, room, event);

  return <>Read ({readBy.length})</>;
};
+15 −21
Original line number Diff line number Diff line
import { getReceiptsForEvent } from "@/lib/getReceiptsForEvent";
import type { MXClient } from "@/lib/MXClient";
import * as sdk from "matrix-js-sdk";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

export const useRead = (
  client: MXClient,
@@ -9,33 +10,26 @@ export const useRead = (
) => {
  const [readBy, setReadBy] = useState<sdk.RoomMember[]>([]);

  useEffect(() => {
    const update = () => {
      const receipts = room.getReceiptsForEvent(event) || [];

      const members = receipts
        .filter((r) => r.type === "m.read")
        .filter((r) => r.userId !== client.client.getUserId())
        .map((r) => room.getMember(r.userId))
        .filter((r): r is sdk.RoomMember => Boolean(r));

  const update = useCallback(() => {
    const members = getReceiptsForEvent(room, event);
    setReadBy(members);
    };
  }, [room, event]);

  useEffect(() => {
    update();

    const handle = (revent: sdk.MatrixEvent, rroom: sdk.Room) => {
      if (rroom.roomId !== room.roomId) return;

    const handle = (_event: sdk.MatrixEvent, rroom?: sdk.Room) => {
      if (rroom?.roomId === room.roomId) {
        update();
      }
    };

    client.client.on(sdk.RoomEvent.Receipt, handle);
    room.client.on(sdk.RoomEvent.Receipt, handle);

    return () => {
      client.client.off(sdk.RoomEvent.Receipt, handle);
      room.client.off(sdk.RoomEvent.Receipt, handle);
    };
  }, [client, room, event]);
  }, [room, event, update]);

  return { readBy };
  return readBy;
};
Loading