Loading packages/chat/lib/components/ChatLog/ChatBubble.tsx +4 −3 Original line number Diff line number Diff line import { MessageFlag, type Message } from "@/lib/const"; import { useYap } from "@/lib/context"; import { useYap } from "@/context/utils"; import type React from "react"; type MessageRole = "ME" | "OTHER" | "SYSTEM"; export const ChatBubble = ({ message }: { message: Message }) => { const { userId, isSystem } = useYap(); const { state } = useYap<"Internal">(); const isSystem = (_str: string) => false; const role: MessageRole = isSystem(message.sender) ? "SYSTEM" : message.sender === userId : message.sender === state.user ? "ME" : "OTHER"; const flags = message.flags; Loading packages/chat/lib/components/Tab/Home.tsx +61 −22 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 { useReducer, type JSX } from "react"; import type { MatrixEvent } from "matrix-js-sdk"; import { MXClient } from "@/lib/MXClient"; const TEST_ROOMS = [ "!room1:cool.chat", "!room2:bad.chat", "!room3:the.chat", ] as const; export const HomeTab = () => { const { dispatch } = useYap<"Internal">(); const { subscriptions, lastEvent } = useSubscriptions(); export const HomeTab = ({ openRoom, }: { openRoom: (roomId: `!${string}:${string}`) => unknown; }) => { return ( <Tab name="Chat" isNagging={false}> <ul> {TEST_ROOMS.map((roomId) => ( {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 key={roomId} roomId={roomId} onClick={() => openRoom(roomId)} onClick={() => dispatch(["openChat", roomId])} /> ))} </ul> Loading @@ -30,9 +39,11 @@ const ChatItem = ({ roomId, onClick, }: { roomId: string; roomId: MXRoomID; onClick: () => unknown; }) => { const { name, recent, membership } = useRoom(roomId); return ( <li className="flex flex-row gap-1 p-1 relative"> <button Loading @@ -43,13 +54,41 @@ const ChatItem = ({ <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">{roomId}</span> <span className="font-bold">{name || roomId}</span> <span className="text-xs text-gray-800 text-ellipsis max-w-full"> hey man you can't place therehey man you can't place therehey man you can't place therehey man you can't place therehey man you can't place there {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/Room.tsx +7 −7 Original line number Diff line number Diff line Loading @@ -2,27 +2,27 @@ import { Tab } from "../Tab"; import { ChatLog } from "../ChatLog/ChatLog"; import type { MXRoomID } from "@/lib/const"; import { useMessages } from "@/hooks/useMessages"; import { useYap } from "@/context/utils"; import { useRoom } from "@/hooks/useRoom"; export const RoomTab = ({ roomId, isNagging, onClose, onFocus, }: { roomId: MXRoomID; isNagging: boolean; onClose: () => unknown; onFocus: () => unknown; }) => { const { dispatch } = useYap<"Internal">(); const room = useRoom(roomId); const { messages, loading, atBeginning, loadMore } = useMessages(roomId); return ( <Tab name={`room: ${roomId}`} name={room.name || `room: ${roomId}`} isNagging={isNagging} className="flex flex-col" onClose={onClose} onFocus={onFocus} onClose={() => dispatch(["closeChat", roomId])} onFocus={() => dispatch(["unnag", roomId])} > <div className="p-2 grow overflow-y-auto"> {/* <ChatBubble role="ME" flags={MessageFlag.SENDING ^ MessageFlag.SEEN} /> Loading packages/chat/lib/components/Yapper.tsx +7 −26 Original line number Diff line number Diff line import type { MXUserID } from "@/lib/const"; import { HomeTab } from "./Tab/Home"; import { RoomTab } from "./Tab/Room"; import { ContextProvider, useYap } from "@/lib/context"; import type { YapController } from "@/YapController"; import { getOpenRooms, useYap } from "@/context/utils"; import { useEffect } from "react"; type Props = { userId: MXUserID; isSystem: (userId: MXUserID) => boolean; }; export const Yapper = ({ controller }: { controller: YapController }) => { return ( <ContextProvider userId={`@:`} isSystem={() => false}> <YapperInner /> </ContextProvider> ); }; const YapperInner = () => { const { state, dispatch } = useYap(); export const Yapper = () => { const { state, dispatch } = useYap<"Internal">(); return ( <div id="chat-overlay" className="absolute bottom-0 right-0 flex gap-1 px-1" > {state.open.map((roomId) => ( <RoomTab key={roomId} roomId={roomId} isNagging={state.nag.indexOf(roomId) > -1} onClose={() => dispatch(["close", roomId])} onFocus={() => dispatch(["unnag", roomId])} /> {getOpenRooms(state).map((room) => ( <RoomTab key={room.roomId} roomId={room.roomId} isNagging={room.nag} /> ))} <HomeTab openRoom={(roomId) => dispatch(["open", roomId])} /> <HomeTab /> </div> ); }; packages/chat/lib/components/YapContext.tsx→packages/chat/lib/context/YapContext.tsx +49 −44 Original line number Diff line number Diff line import type { MXRoomID, MXUserID } from "@/lib/const"; import { MXClient } from "@/lib/MXClient"; import type React from "react"; import { createContext, useCallback, useContext, useMemo, useReducer, } from "react"; type State = { stage: "INIT"; user?: MXUserID; }; type Actions = ["login"] | ["ready"]; type InternalAPI = { state: State; dispatch: React.ActionDispatch<[Actions]>; }; type PublicAPI = { ready: () => unknown; doLogin: () => unknown; openChat: (withWho: MXUserID | MXRoomID) => unknown; setSystem: (handler: (mxid: MXUserID) => boolean) => unknown; }; const context = createContext<InternalAPI & PublicAPI>( // eslint-disable-next-line @typescript-eslint/no-explicit-any null as any, ); // eslint-disable-next-line react-refresh/only-export-components export const useYap: < Mode extends "Internal" | "" = "", >() => Mode extends "Internal" ? InternalAPI & PublicAPI : PublicAPI = () => useContext(context); import { useCallback, useMemo, useReducer } from "react"; import type { Actions, State } from "./types"; import { context } from "./utils"; export const YapContext = ({ children }: React.PropsWithChildren) => { // @ts-expect-error ignore const client = useMemo<MXClient>(() => new MXClient(), []); const client = useMemo<MXClient>(() => MXClient.get(), []); const [state, dispatch] = useReducer<State, [Actions]>( (state, [action, ...data]) => { switch (action) { case "login": window.open(client.getLoginUrl(window.location.href), "_blank"); return state; (state, action) => { switch (action[0]) { case "ready": return state; case "openChat": return { ...state, rooms: { ...state.rooms, [action[1]]: { nag: true, open: true, }, }, }; case "closeChat": return { ...state, rooms: { ...state.rooms, [action[1]]: { ...state.rooms[action[1]], open: false, }, }, }; case "unnag": return { ...state, rooms: { ...state.rooms, [action[1]]: { ...state.rooms[action[1]], nag: false, }, }, }; } return state; }, { stage: "INIT", rooms: {}, }, ); Loading Loading @@ -97,7 +94,15 @@ export const YapContext = ({ children }: React.PropsWithChildren) => { return ( <context.Provider value={{ state, dispatch, doLogin, openChat, setSystem, ready }} value={{ state, client, dispatch, doLogin, openChat, setSystem, ready, }} > {children} </context.Provider> Loading Loading
packages/chat/lib/components/ChatLog/ChatBubble.tsx +4 −3 Original line number Diff line number Diff line import { MessageFlag, type Message } from "@/lib/const"; import { useYap } from "@/lib/context"; import { useYap } from "@/context/utils"; import type React from "react"; type MessageRole = "ME" | "OTHER" | "SYSTEM"; export const ChatBubble = ({ message }: { message: Message }) => { const { userId, isSystem } = useYap(); const { state } = useYap<"Internal">(); const isSystem = (_str: string) => false; const role: MessageRole = isSystem(message.sender) ? "SYSTEM" : message.sender === userId : message.sender === state.user ? "ME" : "OTHER"; const flags = message.flags; Loading
packages/chat/lib/components/Tab/Home.tsx +61 −22 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 { useReducer, type JSX } from "react"; import type { MatrixEvent } from "matrix-js-sdk"; import { MXClient } from "@/lib/MXClient"; const TEST_ROOMS = [ "!room1:cool.chat", "!room2:bad.chat", "!room3:the.chat", ] as const; export const HomeTab = () => { const { dispatch } = useYap<"Internal">(); const { subscriptions, lastEvent } = useSubscriptions(); export const HomeTab = ({ openRoom, }: { openRoom: (roomId: `!${string}:${string}`) => unknown; }) => { return ( <Tab name="Chat" isNagging={false}> <ul> {TEST_ROOMS.map((roomId) => ( {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 key={roomId} roomId={roomId} onClick={() => openRoom(roomId)} onClick={() => dispatch(["openChat", roomId])} /> ))} </ul> Loading @@ -30,9 +39,11 @@ const ChatItem = ({ roomId, onClick, }: { roomId: string; roomId: MXRoomID; onClick: () => unknown; }) => { const { name, recent, membership } = useRoom(roomId); return ( <li className="flex flex-row gap-1 p-1 relative"> <button Loading @@ -43,13 +54,41 @@ const ChatItem = ({ <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">{roomId}</span> <span className="font-bold">{name || roomId}</span> <span className="text-xs text-gray-800 text-ellipsis max-w-full"> hey man you can't place therehey man you can't place therehey man you can't place therehey man you can't place therehey man you can't place there {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/Room.tsx +7 −7 Original line number Diff line number Diff line Loading @@ -2,27 +2,27 @@ import { Tab } from "../Tab"; import { ChatLog } from "../ChatLog/ChatLog"; import type { MXRoomID } from "@/lib/const"; import { useMessages } from "@/hooks/useMessages"; import { useYap } from "@/context/utils"; import { useRoom } from "@/hooks/useRoom"; export const RoomTab = ({ roomId, isNagging, onClose, onFocus, }: { roomId: MXRoomID; isNagging: boolean; onClose: () => unknown; onFocus: () => unknown; }) => { const { dispatch } = useYap<"Internal">(); const room = useRoom(roomId); const { messages, loading, atBeginning, loadMore } = useMessages(roomId); return ( <Tab name={`room: ${roomId}`} name={room.name || `room: ${roomId}`} isNagging={isNagging} className="flex flex-col" onClose={onClose} onFocus={onFocus} onClose={() => dispatch(["closeChat", roomId])} onFocus={() => dispatch(["unnag", roomId])} > <div className="p-2 grow overflow-y-auto"> {/* <ChatBubble role="ME" flags={MessageFlag.SENDING ^ MessageFlag.SEEN} /> Loading
packages/chat/lib/components/Yapper.tsx +7 −26 Original line number Diff line number Diff line import type { MXUserID } from "@/lib/const"; import { HomeTab } from "./Tab/Home"; import { RoomTab } from "./Tab/Room"; import { ContextProvider, useYap } from "@/lib/context"; import type { YapController } from "@/YapController"; import { getOpenRooms, useYap } from "@/context/utils"; import { useEffect } from "react"; type Props = { userId: MXUserID; isSystem: (userId: MXUserID) => boolean; }; export const Yapper = ({ controller }: { controller: YapController }) => { return ( <ContextProvider userId={`@:`} isSystem={() => false}> <YapperInner /> </ContextProvider> ); }; const YapperInner = () => { const { state, dispatch } = useYap(); export const Yapper = () => { const { state, dispatch } = useYap<"Internal">(); return ( <div id="chat-overlay" className="absolute bottom-0 right-0 flex gap-1 px-1" > {state.open.map((roomId) => ( <RoomTab key={roomId} roomId={roomId} isNagging={state.nag.indexOf(roomId) > -1} onClose={() => dispatch(["close", roomId])} onFocus={() => dispatch(["unnag", roomId])} /> {getOpenRooms(state).map((room) => ( <RoomTab key={room.roomId} roomId={room.roomId} isNagging={room.nag} /> ))} <HomeTab openRoom={(roomId) => dispatch(["open", roomId])} /> <HomeTab /> </div> ); };
packages/chat/lib/components/YapContext.tsx→packages/chat/lib/context/YapContext.tsx +49 −44 Original line number Diff line number Diff line import type { MXRoomID, MXUserID } from "@/lib/const"; import { MXClient } from "@/lib/MXClient"; import type React from "react"; import { createContext, useCallback, useContext, useMemo, useReducer, } from "react"; type State = { stage: "INIT"; user?: MXUserID; }; type Actions = ["login"] | ["ready"]; type InternalAPI = { state: State; dispatch: React.ActionDispatch<[Actions]>; }; type PublicAPI = { ready: () => unknown; doLogin: () => unknown; openChat: (withWho: MXUserID | MXRoomID) => unknown; setSystem: (handler: (mxid: MXUserID) => boolean) => unknown; }; const context = createContext<InternalAPI & PublicAPI>( // eslint-disable-next-line @typescript-eslint/no-explicit-any null as any, ); // eslint-disable-next-line react-refresh/only-export-components export const useYap: < Mode extends "Internal" | "" = "", >() => Mode extends "Internal" ? InternalAPI & PublicAPI : PublicAPI = () => useContext(context); import { useCallback, useMemo, useReducer } from "react"; import type { Actions, State } from "./types"; import { context } from "./utils"; export const YapContext = ({ children }: React.PropsWithChildren) => { // @ts-expect-error ignore const client = useMemo<MXClient>(() => new MXClient(), []); const client = useMemo<MXClient>(() => MXClient.get(), []); const [state, dispatch] = useReducer<State, [Actions]>( (state, [action, ...data]) => { switch (action) { case "login": window.open(client.getLoginUrl(window.location.href), "_blank"); return state; (state, action) => { switch (action[0]) { case "ready": return state; case "openChat": return { ...state, rooms: { ...state.rooms, [action[1]]: { nag: true, open: true, }, }, }; case "closeChat": return { ...state, rooms: { ...state.rooms, [action[1]]: { ...state.rooms[action[1]], open: false, }, }, }; case "unnag": return { ...state, rooms: { ...state.rooms, [action[1]]: { ...state.rooms[action[1]], nag: false, }, }, }; } return state; }, { stage: "INIT", rooms: {}, }, ); Loading Loading @@ -97,7 +94,15 @@ export const YapContext = ({ children }: React.PropsWithChildren) => { return ( <context.Provider value={{ state, dispatch, doLogin, openChat, setSystem, ready }} value={{ state, client, dispatch, doLogin, openChat, setSystem, ready, }} > {children} </context.Provider> Loading