Loading packages/chat/lib/components/Tab.tsx +4 −2 Original line number Diff line number Diff line Loading @@ -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 Loading packages/chat/lib/components/Tab/Home.tsx +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; }; }); packages/chat/lib/components/Tab/Invites.tsx 0 → 100644 +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> ); }); packages/chat/lib/components/Tab/Room.tsx +102 −21 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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}`} Loading @@ -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" /> Loading @@ -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> ); }; packages/chat/lib/components/UI/List.tsx 0 → 100644 +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
packages/chat/lib/components/Tab.tsx +4 −2 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/chat/lib/components/Tab/Home.tsx +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; }; });
packages/chat/lib/components/Tab/Invites.tsx 0 → 100644 +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> ); });
packages/chat/lib/components/Tab/Room.tsx +102 −21 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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}`} Loading @@ -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" /> Loading @@ -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> ); };
packages/chat/lib/components/UI/List.tsx 0 → 100644 +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> ); };