Commit e14b05cb authored by Grant's avatar Grant
Browse files

draft: event rendering

parent 671c19bf
Loading
Loading
Loading
Loading
+28 −34
Original line number Diff line number Diff line
import { MessageFlag, type Message } from "@/lib/const";
import { useYap } from "@/context/utils";
import type React from "react";
import type { MatrixEvent } from "matrix-js-sdk";

type MessageRole = "ME" | "OTHER" | "SYSTEM";

export const ChatBubble = ({ message }: { message: Message }) => {
  const { state } = useYap<"Internal">();
  const isSystem = (_str: string) => false;
  const role: MessageRole = isSystem(message.sender)
    ? "SYSTEM"
    : message.sender === state.user
      ? "ME"
      : "OTHER";
  const flags = message.flags;
export const ChatBubble = ({ event }: { event: MatrixEvent }) => {
  const { client } = useYap<"Internal">();
  const isRight = event.getSender() === client.client.getUserId();

  return (
    <div
      className="flex flex-col w-full"
      style={{
        alignItems:
          role === "ME" ? "end" : role === "OTHER" ? "start" : "center",
        alignItems: isRight ? "end" : "start",
      }}
    >
      {role === "SYSTEM" ? (
        <div className="self-center">{message.content}</div>
      ) : (
      {/* {role === "SYSTEM" ? (
        <div className="self-center">{event.content}</div>
      ) : ( */}
      <>
        <div className="flex flex-row gap-1 *:text-xs">
          <span>{event.getSender()}</span>
        </div>
        <div
          className="rounded-xl p-1"
          style={{
              backgroundColor:
                role === "ME"
            backgroundColor: isRight
              ? "var(--color-gray-500)"
              : "var(--color-gray-300)",
          }}
        >
            {message.content}
          {event.getContent().body}
        </div>
        <div className="flex flex-row gap-1 *:text-xs">
            {flags & MessageFlag.SENDING ? <span>sending</span> : ""}
          {/* {flags & MessageFlag.SENDING ? <span>sending</span> : ""}
            {flags & MessageFlag.SEEN ? <span>seen</span> : ""}
            {flags & MessageFlag.EDITED ? <span>edited</span> : ""}
            {flags & MessageFlag.EDITED ? <span>edited</span> : ""} */}
        </div>
      </>
      )}
      {/* )} */}
    </div>
  );
};
+1 −1
Original line number Diff line number Diff line
@@ -26,7 +26,7 @@ export const ChatLog = ({
      )}
      <React.Fragment>
        {messages.map((msg) => (
          <ChatBubble key={msg.eventId} message={msg} />
          <ChatBubble key={msg.eventId} event={msg} />
        ))}
      </React.Fragment>
      <div ref={bottom} />
+68 −0
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 { ChatBubble } from "./ChatBubble";

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(
      client,
      room!.getUnfilteredTimelineSet()!,
    ));
  }

  useEffect(() => {
    const { client } = MXClient.get();

    getTimeline()
      .load()
      .then(() => {
        setEvents(getTimeline().getEvents());
      })
      .catch((e) => {
        console.error(e);
      });

    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]);

  return (
    <ul>
      {events.map((ev) => (
        <li key={ev.getId()}>
          <ChatBubble event={ev} />
        </li>
      ))}
    </ul>
  );
};
+76 −14
Original line number Diff line number Diff line
@@ -4,8 +4,10 @@ import type { MXRoomID } from "@/lib/const";
import { useMessages } from "@/hooks/useMessages";
import { useYap } from "@/context/utils";
import { useRoom } from "@/hooks/useRoom";
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { MXClient } from "@/lib/MXClient";
import { Room } from "matrix-js-sdk";
import { ChatTimeline } from "../ChatLog/ChatTimeline";

export const RoomTab = ({
  roomId,
@@ -57,13 +59,17 @@ export const RoomTab = ({
            {/* <ChatBubble role="ME" flags={MessageFlag.SENDING ^ MessageFlag.SEEN} />
      <ChatBubble role="SYSTEM" />
      <ChatBubble role="OTHER" /> */}
            <ChatLog
            {/* <ChatLog
              messages={messages}
              loading={loading}
              atBeginning={atBeginning}
              loadMore={loadMore}
            />
            /> */}
            <ChatTimeline roomId={roomId} />
          </div>
          {room.replacedBy ? (
            <ReplacedByBanner roomId={roomId} />
          ) : (
            <div className="p-2">
              <form action="#" onSubmit={sendMessage}>
                <input
@@ -75,12 +81,68 @@ export const RoomTab = ({
                <button type="submit">Send</button>
              </form>
            </div>
          )}
        </>
      )}
    </Tab>
  );
};

const ReplacedByBanner = ({ roomId }: { roomId: MXRoomID }) => {
  const { dispatch } = useYap<"Internal">();
  const { replacedBy } = useRoom(roomId);
  const [room, setRoom] = useState<Room>();

  useEffect(() => {
    const mxclient = MXClient.get();

    const onDynamic = (room: Room) => {
      if (replacedBy && room.roomId === replacedBy) {
        setRoom(room);
      }
    };

    if (replacedBy) {
      setRoom(mxclient.rooms.getRoom(replacedBy) ?? undefined);
    }

    mxclient.rooms.on("new", onDynamic);

    return () => {
      mxclient.rooms.off("new", onDynamic);
    };
  }, [replacedBy]);

  const doJoin = useCallback(
    (oldRoomId: MXRoomID, newRoomId: MXRoomID) => {
      const mxclient = MXClient.get();
      mxclient.rooms
        .joinRoom(newRoomId)
        .then(() => {
          dispatch(["openChat", newRoomId]);
          dispatch(["closeChat", oldRoomId]);
        })
        .catch((e) => {
          console.error("failed to join replacement room", e);
        });
    },
    [dispatch],
  );

  if (!replacedBy) return null;

  return (
    <div className="bg-red-400/50 p-1 w-full overflow-x-clip relative">
      room replaced by {room?.name ?? replacedBy}
      <button
        className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
        title="Join new room"
        onClick={() => doJoin(roomId, replacedBy)}
      ></button>
    </div>
  );
};

const ConfirmInvite = ({
  roomId,
  onDecline,
+36 −44
Original line number Diff line number Diff line
import { MessageFlag, type Message, type MXRoomID } from "@/lib/const";
import { useCallback, useState } from "react";

/**
 * sorted oldest to newest
 */
const TEST_MESSAGES: Message[] = [
  {
    eventId: `$a`,
    sender: "@grant:aftermath.gg",
    content: "hello",
    flags: MessageFlag.SEEN,
    createdAt: "2026-03-30T17:40:46.659Z",
  },
  {
    eventId: `$b`,
    sender: "@tnarg:aftermath.gg",
    content: "canvas!",
    flags: 0,
    createdAt: "2026-03-30T17:41:46.659Z",
  },
  {
    eventId: "$c",
    sender: "@canvas:aftermath.gg",
    content: "canvas 2026 has started",
    flags: 0,
    createdAt: "2026-03-30T17:45:46.659Z",
  },
  {
    eventId: "$d",
    sender: "@tnarg:aftermath.gg",
    content: "lets gooo",
    flags: 0,
    createdAt: "2026-03-30T17:46:46.659Z",
  },
  {
    eventId: "$e",
    sender: "@grant:aftermath.gg",
    content: "very cool",
    flags: MessageFlag.SENDING,
    createdAt: "2026-03-30T17:47:46.659Z",
  },
];
import {
  MessageFlag,
  type Message,
  type MXEventID,
  type MXRoomID,
  type MXUserID,
} from "@/lib/const";
import { MXClient } from "@/lib/MXClient";
import { useCallback, useEffect, useState } from "react";

export const useMessages = (
  _roomId: MXRoomID,
  roomId: MXRoomID,
): {
  messages: Message[];
  loading: boolean;
@@ -55,6 +21,32 @@ export const useMessages = (
  const [messages, setMessages] = useState<Message[]>([]);
  const [cursor, setCursor] = useState<string>();

  useEffect(() => {
    const mxclient = MXClient.get();
    const room = mxclient.rooms.getRoom(roomId);

    if (!room) {
      console.warn("useMessages: failed to get room " + roomId);
      return;
    }

    const events = room.getLiveTimeline().getEvents();
    const messages = events.filter((e) => e.getType() === "m.room.message");

    setMessages(
      messages.map(
        (mxevent): Message => ({
          eventId: mxevent.getId()! as MXEventID,
          content: mxevent.getContent().body,
          createdAt: mxevent.getDate()!.toISOString(),
          sender: mxevent.getSender()! as MXUserID,
          event: mxevent,
          flags: 0,
        }),
      ),
    );
  }, [roomId]);

  const loadMore = useCallback(() => {
    if (loading) return;

Loading