Commit bbc5ea94 authored by Grant's avatar Grant
Browse files

draft: room invites & sending messages

parent 3063a51b
Loading
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -4,18 +4,20 @@ import { useState } from "react";
export const Tab = ({
  name,
  isNagging,
  defaultOpen,
  className,
  onClose,
  onFocus,
  children,
}: React.PropsWithChildren<{
  name: string;
  isNagging: boolean;
  isNagging?: boolean;
  defaultOpen?: boolean;
  className?: string;
  onClose?: () => unknown;
  onFocus?: () => unknown;
}>) => {
  const [open, setOpen] = useState(false);
  const [open, setOpen] = useState(defaultOpen ?? false);

  return (
    <div
+46 −81
Original line number Diff line number Diff line
import { useYap } from "@/context/utils";
import { Tab } from "../Tab";
import { useSubscriptions } from "@/hooks/useSubscriptions";
import { useRoom } from "@/hooks/useRoom";
import type { MXRoomID } from "@/main";
import { type JSX } from "react";
import { memo, useEffect, useState, type JSX } from "react";
import type { MatrixEvent } from "matrix-js-sdk";
import * as sdk from "matrix-js-sdk";
import { MXClient } from "@/lib/MXClient";
import { RoomListItem } from "../UI/RoomListItem";
import { List } from "../UI/List";

export const HomeTab = () => {
export const HomeTab = memo(() => {
  const { dispatch } = useYap<"Internal">();
  const { subscriptions, lastEvent } = useSubscriptions();
  const [invites, setInvites] = useState(0);
  const [subs, setSubs] = useState<{ roomId: MXRoomID }[]>([]);

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

    const onSync = (rooms: sdk.Room[]) => {
      setSubs(rooms.map((r) => ({ roomId: r.roomId as MXRoomID })));
    };
    const onInvites = (rooms: sdk.Room[]) => {
      setInvites(rooms.length);
    };

    setInvites(rooms.Invites.get().length);
    setSubs(
      rooms.Subscriptions.get().map((r) => ({ roomId: r.roomId as MXRoomID })),
    );

    rooms.Subscriptions.on("sync", onSync);
    rooms.Invites.on("sync", onInvites);

    return () => {
      rooms.Subscriptions.off("sync", onSync);
      rooms.Invites.off("sync", onInvites);
    };
  }, []);

  return (
    <Tab name="Chat" isNagging={false}>
      <ul>
        {Array.from(subscriptions)
          .sort((a, b) => {
            const aLast = lastEvent[a];
            const bLast = lastEvent[b];
            return (
              (bLast?.getDate()?.getTime() ?? 0) -
              (aLast?.getDate()?.getTime() ?? 0)
            );
          })
          .map((roomId) => (
            <ChatItem
      <List>
        {invites > 0 && (
          <List.Item onClick={() => dispatch(["open", "invites"])}>
            invites {invites}
          </List.Item>
        )}
        {subs.map(({ roomId }) => (
          <RoomListItem
            key={roomId}
            roomId={roomId}
            onClick={() => dispatch(["openChat", roomId])}
          />
        ))}
      </ul>
      </List>
    </Tab>
  );
};

const ChatItem = ({
  roomId,
  onClick,
}: {
  roomId: MXRoomID;
  onClick: () => unknown;
}) => {
  const { name, recent, membership } = useRoom(roomId);

  return (
    <li className="flex flex-row gap-1 p-1 relative">
      <button
        className="absolute top-0 left-0 w-full h-full cursor-pointer"
        onClick={onClick}
      ></button>
      <div className="flex items-center justify-center p-0.5">
        <div className="w-8 h-8 shrink-0 rounded-xl bg-gray-500"></div>
      </div>
      <div className="flex flex-col gap-px">
        <span className="font-bold">{name || roomId}</span>
        <span className="text-xs text-gray-800 text-ellipsis max-w-full">
          {membership}
          <ShortEvent event={recent} />
        </span>
      </div>
    </li>
  );
};

const ShortEvent = ({ event }: { event?: MatrixEvent }) => {
  const { client } = MXClient.get();

  if (!event) return null;

  const content: JSX.Element[] = [];

  if (event.getSender() === client.getUserId()) {
    content.push(
      <>
        <strong>You:</strong>{" "}
      </>,
    );
  }

  switch (event.getType()) {
    case "m.room.encrypted":
      content.push(<em>TODO: Encryption</em>);
      break;
    case "m.room.message":
      content.push(<>{event.getContent().body}</>);
      break;
    default:
      content.push(<em>Unknown: {event.getType()}</em>);
  }

  return content;
};
});
+67 −0
Original line number Diff line number Diff line
import { useYap } from "@/context/utils";
import { memo, useEffect, useState } from "react";
import { Tab } from "../Tab";
import type { MXRoomID, MXUserID } from "@/main";
import { MXClient } from "@/lib/MXClient";
import * as sdk from "matrix-js-sdk";
import { List } from "../UI/List";
import { RoomListItem } from "../UI/RoomListItem";

const getInviter = (room: sdk.Room): MXUserID | undefined =>
  room
    .getMember(MXClient.get().client.getUserId()!)
    ?.events.member?.getSender() as MXUserID;

export const InvitesTab = memo(() => {
  const { dispatch } = useYap<"Internal">();
  const [invites, setInvites] = useState<
    { roomId: MXRoomID; inviter?: MXUserID }[]
  >([]);

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

    const onSync = (rooms: sdk.Room[]) => {
      setInvites(
        rooms.map((r) => ({
          roomId: r.roomId as MXRoomID,
          inviter: getInviter(r),
        })),
      );
    };

    setInvites(
      rooms.Invites.get().map((r) => ({
        roomId: r.roomId as MXRoomID,
        inviter: getInviter(r),
      })),
    );

    rooms.Invites.on("sync", onSync);

    return () => {
      rooms.Invites.off("sync", onSync);
    };
  }, []);

  return (
    <Tab
      name="Invites"
      isNagging={false}
      defaultOpen
      onClose={() => dispatch(["close", "invites"])}
    >
      {invites.length === 0 && <span>no invites!</span>}
      <List>
        {invites.map(({ roomId, inviter }) => (
          <RoomListItem
            key={roomId}
            roomId={roomId}
            subtitle={inviter}
            onClick={() => dispatch(["openChat", roomId])}
          />
        ))}
      </List>
    </Tab>
  );
});
+102 −21
Original line number Diff line number Diff line
@@ -4,6 +4,8 @@ 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 { MXClient } from "@/lib/MXClient";

export const RoomTab = ({
  roomId,
@@ -14,8 +16,27 @@ 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) => {
      e.preventDefault();

      const mxclient = MXClient.get();
      mxclient
        .sendMessage(roomId, message)
        .then((event) => {
          console.log("sent message", event);
          setMessage("");
        })
        .catch((e) => {
          console.error(e);
        });
    },
    [message, roomId],
  );

  return (
    <Tab
      name={room.name || `room: ${roomId}`}
@@ -24,6 +45,14 @@ export const RoomTab = ({
      onClose={() => dispatch(["closeChat", roomId])}
      onFocus={() => dispatch(["unnag", roomId])}
    >
      {room.membership === "invite" && (
        <ConfirmInvite
          roomId={roomId}
          onDecline={() => dispatch(["closeChat", roomId])}
        />
      )}
      {room.membership === "join" && (
        <>
          <div className="p-2 grow overflow-y-auto">
            {/* <ChatBubble role="ME" flags={MessageFlag.SENDING ^ MessageFlag.SEEN} />
      <ChatBubble role="SYSTEM" />
@@ -36,16 +65,68 @@ export const RoomTab = ({
            />
          </div>
          <div className="p-2">
        <form
          action="#"
          onSubmit={(e) => {
            e.preventDefault();
          }}
        >
          <input type="text" placeholder="New Message" />
            <form action="#" onSubmit={sendMessage}>
              <input
                type="text"
                placeholder="New Message"
                value={message}
                onChange={(e) => setMessage(e.currentTarget.value)}
              />
              <button type="submit">Send</button>
            </form>
          </div>
        </>
      )}
    </Tab>
  );
};

const ConfirmInvite = ({
  roomId,
  onDecline,
}: {
  onDecline: () => unknown;
  roomId: MXRoomID;
}) => {
  const room = useRoom(roomId);

  const doAccept = useCallback(() => {
    const mxclient = MXClient.get();
    mxclient.rooms
      .joinRoom(roomId)
      .then(() => {
        console.log("joined room");
      })
      .catch((e) => {
        console.error(e);
      });
  }, [roomId]);

  const doDecline = useCallback(() => {
    const mxclient = MXClient.get();
    mxclient.rooms
      .leaveRoom(roomId)
      .then(() => {
        console.log("left room");
        onDecline();
      })
      .catch((e) => {
        console.error(e);
      });
  }, [onDecline, roomId]);

  return (
    <div className="max-w-full text-wrap">
      @user:name.com invited you to {room.name}
      <button
        className="text-green-700 border-2 rounded p-1"
        onClick={doAccept}
      >
        accept
      </button>
      <button className="text-red-700 border-2 rounded p-1" onClick={doDecline}>
        decline
      </button>
    </div>
  );
};
+49 −0
Original line number Diff line number Diff line
import type { HTMLProps, MouseEventHandler } from "react";

export const List = ({ children, ...props }: HTMLProps<HTMLUListElement>) => {
  return <ul {...props}>{children}</ul>;
};

export type ListItemProps = {
  title?: React.ReactNode;
  subtitle?: React.ReactNode;
  avatar?: React.ReactNode | null;
  onClick?: MouseEventHandler<HTMLButtonElement>;
} & HTMLProps<HTMLLIElement>;

List.Item = ({
  children,
  title,
  subtitle,
  avatar,
  onClick,
  ...props
}: ListItemProps) => {
  return (
    <li
      {...props}
      className={"flex flex-row gap-1 p-1 relative " + props.className}
    >
      <button
        className="absolute top-0 left-0 w-full h-full cursor-pointer"
        onClick={onClick}
      ></button>
      {typeof avatar !== "undefined" && (
        <div className="flex items-center justify-center p-0.5">
          <div className="w-8 h-8 shrink-0 rounded-xl bg-gray-500"></div>
        </div>
      )}
      {(title || subtitle) && (
        <div className="flex flex-col gap-px">
          <span className="font-bold">{title}</span>
          {subtitle && (
            <span className="text-xs text-gray-800 text-ellipsis max-w-full">
              {subtitle}
            </span>
          )}
        </div>
      )}
      {children}
    </li>
  );
};
Loading