From 3991b2685becbe8bd9360eb07fbed6741702969d Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 5 Aug 2026 23:24:25 -0600 Subject: [PATCH 1/3] more work: - dompurify - timeline syncing - pagination of timeline - initial read receipts --- .../lib/components/ChatLog/ChatBubble.tsx | 116 +++++++++++------ .../lib/components/ChatLog/ChatTimeline.tsx | 102 +++++++-------- .../lib/components/ChatLog/MessageBody.tsx | 21 +++ .../lib/components/ChatLog/ReadReceipts.tsx | 17 +++ packages/chat/lib/components/Tab/Room.tsx | 9 +- .../chat/lib/components/UI/ShortEvent.tsx | 22 +++- packages/chat/lib/hooks/useRead.ts | 41 ++++++ packages/chat/lib/hooks/useRoom.ts | 17 ++- packages/chat/lib/hooks/useTimeline.ts | 99 ++++++++++++++ packages/chat/lib/lib/MXClient.ts | 2 +- packages/chat/lib/lib/MXRooms.ts | 20 ++- packages/chat/lib/main.ts | 1 + packages/chat/package.json | 4 +- yarn.lock | 121 ++++++++++-------- 14 files changed, 425 insertions(+), 167 deletions(-) create mode 100644 packages/chat/lib/components/ChatLog/MessageBody.tsx create mode 100644 packages/chat/lib/components/ChatLog/ReadReceipts.tsx create mode 100644 packages/chat/lib/hooks/useRead.ts create mode 100644 packages/chat/lib/hooks/useTimeline.ts diff --git a/packages/chat/lib/components/ChatLog/ChatBubble.tsx b/packages/chat/lib/components/ChatLog/ChatBubble.tsx index eeece64..78efe34 100644 --- a/packages/chat/lib/components/ChatLog/ChatBubble.tsx +++ b/packages/chat/lib/components/ChatLog/ChatBubble.tsx @@ -1,41 +1,85 @@ +import React, { useState, useEffect } from "react"; import { useYap } from "@/context/utils"; -import type { MatrixEvent } from "matrix-js-sdk"; +import * as sdk from "matrix-js-sdk"; +import { MessageBody } from "./MessageBody"; +import { ReadReceipts } from "./ReadReceipts"; -export const ChatBubble = ({ event }: { event: MatrixEvent }) => { - const { client } = useYap<"Internal">(); - const isRight = event.getSender() === client.client.getUserId(); +export const ChatBubble = React.memo( + ({ + room, + event, + isLatest, + }: { + room: sdk.Room; + event: sdk.MatrixEvent; + isLatest: boolean; + }) => { + 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()); - return ( -
- {/* {role === "SYSTEM" ? ( + useEffect(() => { + const update = () => { + setContent(event.getContent()); + setEdited(!!event.replacingEvent()); + }; + + event.on(sdk.MatrixEventEvent.Decrypted, update); + event.on(sdk.MatrixEventEvent.Replaced, update); + + return () => { + event.off(sdk.MatrixEventEvent.Decrypted, update); + event.off(sdk.MatrixEventEvent.Replaced, update); + }; + }, [event]); + + return ( +
+ {/* {role === "SYSTEM" ? (
{event.content}
) : ( */} - <> -
- {event.getSender()} -
-
- {event.getContent().body} -
-
- {/* {flags & MessageFlag.SENDING ? sending : ""} - {flags & MessageFlag.SEEN ? seen : ""} - {flags & MessageFlag.EDITED ? edited : ""} */} -
- - {/* )} */} -
- ); -}; + <> +
+ + {event.getSender() && room.getMember(event.getSender()!)?.name} + +
+
+

+ +

+
+
+ {edited && edited} + {event.isSending() && sending...} + {isLatest && ( + + )} +
+ + {/* )} */} +
+ ); + }, +); diff --git a/packages/chat/lib/components/ChatLog/ChatTimeline.tsx b/packages/chat/lib/components/ChatLog/ChatTimeline.tsx index 8363e95..96a0d09 100644 --- a/packages/chat/lib/components/ChatLog/ChatTimeline.tsx +++ b/packages/chat/lib/components/ChatLog/ChatTimeline.tsx @@ -1,68 +1,58 @@ 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 * as sdk from "matrix-js-sdk"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ChatBubble } from "./ChatBubble"; - -export const ChatTimeline = ({ roomId }: { roomId: MXRoomID }) => { - const timelineRef = useRef(null); - const [events, setEvents] = useState([]); - - 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()!, - )); - } +import { useTimeline } from "@/hooks/useTimeline"; + +export const ChatTimeline = ({ + client, + room, +}: { + client: MXClient; + room: sdk.Room; +}) => { + const bottom = useRef(null); + const { events, forward, backward, loading } = useTimeline(client, room); + + const scrollBottom = useCallback(() => { + bottom.current?.scrollIntoView({ behavior: "smooth" }); + }, []); useEffect(() => { - const { client } = MXClient.get(); + scrollBottom(); + }, [events, scrollBottom]); - 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]); + useEffect(() => { + bottom.current?.scrollIntoView({ behavior: "instant" }); + }, []); return (
    - {events.map((ev) => ( -
  • - -
  • - ))} +
  • backward()}>top {loading.backward && "loading"}
  • + {events + .filter( + (ev) => + !ev.isRelation("m.replace") && + !ev.isRelation("m.annotation") && + !ev.isRedaction() && + !ev.isRedacted(), + ) + .map((ev, i, arr) => ( +
  • console.log(ev)}> + {ev.getType()} + {ev.getType() === "m.room.message" && ( + + )} +
  • + ))} +
  • forward()} ref={bottom}> + bottom {loading.forward && "loading"} +
); }; diff --git a/packages/chat/lib/components/ChatLog/MessageBody.tsx b/packages/chat/lib/components/ChatLog/MessageBody.tsx new file mode 100644 index 0000000..7bc3255 --- /dev/null +++ b/packages/chat/lib/components/ChatLog/MessageBody.tsx @@ -0,0 +1,21 @@ +import React, { useEffect, useMemo, useState } from "react"; +import * as sdk from "matrix-js-sdk"; +import DOMPurify from "dompurify"; + +export const MessageBody = ({ event }: { event: sdk.MatrixEvent }) => { + const content = event.getContent(); + + const rendered = useMemo(() => { + if (content.format === "org.matrix.custom.html" && content.formatted_body) { + const html = DOMPurify.sanitize(content.formatted_body, { + ADD_FORBID_CONTENTS: ["mx-reply"], + }); + + return ; + } + + return {content.body}; + }, [content]); + + return <>{rendered}; +}; diff --git a/packages/chat/lib/components/ChatLog/ReadReceipts.tsx b/packages/chat/lib/components/ChatLog/ReadReceipts.tsx new file mode 100644 index 0000000..c4b332d --- /dev/null +++ b/packages/chat/lib/components/ChatLog/ReadReceipts.tsx @@ -0,0 +1,17 @@ +import { useRead } from "@/hooks/useRead"; +import type { MXClient } from "@/lib/MXClient"; +import * as sdk from "matrix-js-sdk"; + +export const ReadReceipts = ({ + client, + room, + event, +}: { + client: MXClient; + room: sdk.Room; + event: sdk.MatrixEvent; +}) => { + const { readBy } = useRead(client, room, event); + + return <>Read ({readBy.length}); +}; diff --git a/packages/chat/lib/components/Tab/Room.tsx b/packages/chat/lib/components/Tab/Room.tsx index 6853219..8fb13c8 100644 --- a/packages/chat/lib/components/Tab/Room.tsx +++ b/packages/chat/lib/components/Tab/Room.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { MXClient } from "@/lib/MXClient"; import { Room } from "matrix-js-sdk"; import { ChatTimeline } from "../ChatLog/ChatTimeline"; +import { useTimeline } from "@/hooks/useTimeline"; export const RoomTab = ({ roomId, @@ -19,7 +20,6 @@ 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) => { @@ -44,6 +44,7 @@ export const RoomTab = ({ name={room.name || `room: ${roomId}`} isNagging={isNagging} className="flex flex-col" + defaultOpen onClose={() => dispatch(["closeChat", roomId])} onFocus={() => dispatch(["unnag", roomId])} > @@ -55,7 +56,7 @@ export const RoomTab = ({ )} {room.membership === "join" && ( <> -
+
{/* */} @@ -65,7 +66,9 @@ export const RoomTab = ({ atBeginning={atBeginning} loadMore={loadMore} /> */} - + {room.room && ( + + )}
{room.replacedBy ? ( diff --git a/packages/chat/lib/components/UI/ShortEvent.tsx b/packages/chat/lib/components/UI/ShortEvent.tsx index 7c48731..00bc38a 100644 --- a/packages/chat/lib/components/UI/ShortEvent.tsx +++ b/packages/chat/lib/components/UI/ShortEvent.tsx @@ -1,6 +1,7 @@ import { MXClient } from "@/lib/MXClient"; import type { MatrixEvent } from "matrix-js-sdk"; import type { JSX } from "react"; +import React from "react"; export const ShortEvent = ({ event }: { event?: MatrixEvent }) => { const { client } = MXClient.get(); @@ -11,21 +12,32 @@ export const ShortEvent = ({ event }: { event?: MatrixEvent }) => { if (event.getSender() === client.getUserId()) { content.push( - <> + You:{" "} - , + , ); } switch (event.getType()) { case "m.room.encrypted": - content.push(TODO: Encryption); + content.push(TODO: Encryption); break; case "m.room.message": - content.push(<>{event.getContent().body}); + content.push( + + {event.getContent().body} + , + ); + break; + case "m.room.member": + content.push( + + {event.getContent().membership} {event.getStateKey()} + , + ); break; default: - content.push(Unknown: {event.getType()}); + content.push(Unknown: {event.getType()}); } return content; diff --git a/packages/chat/lib/hooks/useRead.ts b/packages/chat/lib/hooks/useRead.ts new file mode 100644 index 0000000..0da821c --- /dev/null +++ b/packages/chat/lib/hooks/useRead.ts @@ -0,0 +1,41 @@ +import type { MXClient } from "@/lib/MXClient"; +import * as sdk from "matrix-js-sdk"; +import { useEffect, useState } from "react"; + +export const useRead = ( + client: MXClient, + room: sdk.Room, + event: sdk.MatrixEvent, +) => { + const [readBy, setReadBy] = useState([]); + + 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)); + + setReadBy(members); + }; + + update(); + + const handle = (revent: sdk.MatrixEvent, rroom: sdk.Room) => { + if (rroom.roomId !== room.roomId) return; + + update(); + }; + + client.client.on(sdk.RoomEvent.Receipt, handle); + + return () => { + client.client.off(sdk.RoomEvent.Receipt, handle); + }; + }, [client, room, event]); + + return { readBy }; +}; diff --git a/packages/chat/lib/hooks/useRoom.ts b/packages/chat/lib/hooks/useRoom.ts index dab9169..2f90abf 100644 --- a/packages/chat/lib/hooks/useRoom.ts +++ b/packages/chat/lib/hooks/useRoom.ts @@ -7,22 +7,25 @@ import { Room, RoomEvent, } from "matrix-js-sdk"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; export const useRoom = (roomId: MXRoomID) => { const [name, setName] = useState(); const [recent, setRecent] = useState(); const [membership, setMembership] = useState(); const [replacedBy, setReplacedBy] = useState(); + const [room, setRoom] = useState(null); useEffect(() => { const { client } = MXClient.get(); - const room = client.getRoom(roomId); - setName(room?.name); - setMembership(room?.getMyMembership()); - setRecent(room?.getLiveTimeline().getEvents().at(-1)); + const mxroom = client.getRoom(roomId); + setRoom(mxroom); + + setName(mxroom?.name); + setMembership(mxroom?.getMyMembership()); + setRecent(mxroom?.getLiveTimeline().getEvents().at(-1)); setReplacedBy( - room + mxroom ?.getLiveTimeline() .getState(Direction.Forward) ?.getStateEvents(EventType.RoomTombstone, "") @@ -56,5 +59,5 @@ export const useRoom = (roomId: MXRoomID) => { }; }, [roomId]); - return { name, recent, membership, replacedBy }; + return { name, recent, membership, replacedBy, room }; }; diff --git a/packages/chat/lib/hooks/useTimeline.ts b/packages/chat/lib/hooks/useTimeline.ts new file mode 100644 index 0000000..d8d66b2 --- /dev/null +++ b/packages/chat/lib/hooks/useTimeline.ts @@ -0,0 +1,99 @@ +import type { MXClient } from "@/lib/MXClient"; +import * as sdk from "matrix-js-sdk"; +import { useCallback, useEffect, useState, useReducer } from "react"; + +export function useTimeline(client: MXClient, room: sdk.Room) { + const [timelineWindow, setTimelineWindow] = useState(); + const [events, setEvents] = useState([]); + const [loading, dLoading] = useReducer( + (prev, action: ["start" | "end", "backward" | "forward"]) => ({ + ...prev, + [action[1]]: action[0] === "start", + }), + { + backward: false, + forward: false, + }, + ); + + const sync = useCallback(() => { + if (timelineWindow) { + setEvents([...timelineWindow.getEvents()]); + } + }, [timelineWindow]); + + useEffect(() => { + const tlwindow = new sdk.TimelineWindow( + client.client, + room!.getUnfilteredTimelineSet()!, + ); + + tlwindow.load(undefined, 20).then(() => { + setEvents([...tlwindow.getEvents()]); + }); + + setTimelineWindow(tlwindow); + }, [client, room]); + + useEffect(() => { + if (!client || !room || !timelineWindow) return; + + const handleTimeline = ( + event: sdk.MatrixEvent, + eventRoom: sdk.Room | undefined, + toStartOfTimeline: boolean | undefined, + ) => { + if (eventRoom && eventRoom.roomId !== room.roomId) return; + + if (toStartOfTimeline) return; + + if (timelineWindow) { + if (timelineWindow.canPaginate(sdk.EventTimeline.FORWARDS)) { + timelineWindow.paginate(sdk.EventTimeline.FORWARDS, 10).then(() => { + setEvents([...timelineWindow.getEvents()]); + }); + } else { + setEvents([...timelineWindow.getEvents()]); + } + } + }; + + client.client.on(sdk.RoomEvent.Timeline, handleTimeline); + client.client.on(sdk.RoomEvent.TimelineReset, sync); + client.client.on(sdk.RoomEvent.LocalEchoUpdated, sync); + client.client.on(sdk.RoomEvent.Redaction, sync); + + return () => { + client.client.off(sdk.RoomEvent.Timeline, handleTimeline); + client.client.off(sdk.RoomEvent.TimelineReset, sync); + client.client.off(sdk.RoomEvent.LocalEchoUpdated, sync); + client.client.off(sdk.RoomEvent.Redaction, sync); + }; + }, [client, room, timelineWindow, sync]); + + const forward = async () => { + if (timelineWindow?.canPaginate(sdk.EventTimeline.FORWARDS)) { + dLoading(["start", "forward"]); + await timelineWindow + .paginate(sdk.EventTimeline.FORWARDS, 20) + .finally(() => { + dLoading(["end", "forward"]); + }); + sync(); + } + }; + + const backward = async () => { + if (timelineWindow?.canPaginate(sdk.EventTimeline.BACKWARDS)) { + dLoading(["start", "backward"]); + await timelineWindow + .paginate(sdk.EventTimeline.BACKWARDS, 20) + .finally(() => { + dLoading(["end", "backward"]); + }); + sync(); + } + }; + + return { events, forward, backward, loading }; +} diff --git a/packages/chat/lib/lib/MXClient.ts b/packages/chat/lib/lib/MXClient.ts index 6685423..957b3bf 100644 --- a/packages/chat/lib/lib/MXClient.ts +++ b/packages/chat/lib/lib/MXClient.ts @@ -118,7 +118,7 @@ export class MXClient extends EventEmitter { this.client .startClient() .then(() => { - console.log("client started"); + console.log("client started as " + this.client.getUserId()); this.emit("me", { userId: this.client.getUserId() as MXUserID }); }) .catch((e) => console.error("client error", e)); diff --git a/packages/chat/lib/lib/MXRooms.ts b/packages/chat/lib/lib/MXRooms.ts index 3e36637..f6b4e3a 100644 --- a/packages/chat/lib/lib/MXRooms.ts +++ b/packages/chat/lib/lib/MXRooms.ts @@ -22,7 +22,11 @@ interface Events { ): void; } -const Sort$lastEvent = (a: sdk.Room, b: sdk.Room) => +interface ISort { + (a: sdk.Room, b: sdk.Room): number; +} + +const Sort$lastEvent: ISort = (a, b) => (b.getLastLiveEvent()?.localTimestamp ?? 0) - (a.getLastLiveEvent()?.localTimestamp ?? 0); @@ -136,7 +140,7 @@ class MXRooms$Filter extends EventEmitter<{ sync(data: sdk.Room[]): void }> { constructor( rooms: MXRooms, filter: (room: sdk.Room) => boolean, - compare: (a: sdk.Room, b: sdk.Room) => number, + compare: ISort, ) { super(); @@ -178,6 +182,18 @@ class MXRooms$Filter extends EventEmitter<{ sync(data: sdk.Room[]): void }> { maybeSort(); } }); + + // rooms["MXClient"].client.on( + // sdk.RoomEvent.Timeline, + // (event, room, toStart) => { + // if (toStart) return; + // if (!room) return; + + // if (filter(room)) { + // maybeSort(); + // } + // }, + // ); } get() { diff --git a/packages/chat/lib/main.ts b/packages/chat/lib/main.ts index 7690a8a..7357602 100644 --- a/packages/chat/lib/main.ts +++ b/packages/chat/lib/main.ts @@ -1,2 +1,3 @@ export { YapContext } from "./context/YapContext"; export type { MXEventID, MXRoomID, MXUserID } from "./lib/const"; +export { Yapper } from "./components/Yapper"; diff --git a/packages/chat/package.json b/packages/chat/package.json index 74b07c0..2f15ebc 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -26,15 +26,17 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.2", + "dompurify": "^3.4.13", "eventemitter3": "^5.0.4", "lodash.throttle": "^4.1.1", - "matrix-js-sdk": "^39.4.0", + "matrix-js-sdk": "^42.1.0", "react": "^19.2.7", "react-dom": "^19.2.7", "tailwindcss": "^4.3.2" }, "devDependencies": { "@eslint/js": "^9.39.5", + "@types/dompurify": "^3.2.0", "@types/lodash.throttle": "^4.1.9", "@types/node": "^24.13.3", "@types/react": "^19.2.17", diff --git a/yarn.lock b/yarn.lock index edfb32a..7ffa354 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1631,6 +1631,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^8.0.0": + version: 8.0.0 + resolution: "@babel/runtime@npm:8.0.0" + checksum: 10c0/450857cf52b3a3935d4aad505277201361c86185dad3bdd2ae083d78b76c6265c7fc7610e170c80de1b825d5bfdcfb50697f0853fff922b2661c91cff1fbbd81 + languageName: node + linkType: hard + "@babel/template@npm:^7.28.6": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" @@ -4306,10 +4313,10 @@ __metadata: languageName: node linkType: hard -"@matrix-org/matrix-sdk-crypto-wasm@npm:^15.3.0": - version: 15.3.0 - resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:15.3.0" - checksum: 10c0/45628f36b7b0e54a8777ae67a7233dbdf3e3cf14e0d95d21f62f89a7ea7e3f907232f1eb7b1262193b1e227759fad47af829dcccc103ded89011f13c66f01d76 +"@matrix-org/matrix-sdk-crypto-wasm@npm:^18.4.0": + version: 18.4.0 + resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:18.4.0" + checksum: 10c0/e001147444aeb5a751b23a7223b0d3516b370d417637c7ff29473e52b40d0ea333e88c8f45cc9b79ca730883ef66c483f401331673bedcc53db94a531b45605c languageName: node linkType: hard @@ -8517,18 +8524,20 @@ __metadata: dependencies: "@eslint/js": "npm:^9.39.5" "@tailwindcss/vite": "npm:^4.3.2" + "@types/dompurify": "npm:^3.2.0" "@types/lodash.throttle": "npm:^4.1.9" "@types/node": "npm:^24.13.3" "@types/react": "npm:^19.2.17" "@types/react-dom": "npm:^19.2.3" "@vitejs/plugin-react": "npm:^5.2.0" + dompurify: "npm:^3.4.13" eslint: "npm:^9.39.5" eslint-plugin-react-hooks: "npm:^5.2.0" eslint-plugin-react-refresh: "npm:^0.4.26" eventemitter3: "npm:^5.0.4" globals: "npm:^16.5.0" lodash.throttle: "npm:^4.1.1" - matrix-js-sdk: "npm:^39.4.0" + matrix-js-sdk: "npm:^42.1.0" react: "npm:^19.2.7" react-dom: "npm:^19.2.7" tailwindcss: "npm:^4.3.2" @@ -9644,6 +9653,15 @@ __metadata: languageName: node linkType: hard +"@types/dompurify@npm:^3.2.0": + version: 3.2.0 + resolution: "@types/dompurify@npm:3.2.0" + dependencies: + dompurify: "npm:*" + checksum: 10c0/e83fec586ffbb1b43f7c8479f2a99c223814d240db912d53fb126aaeea52a4091deceeb75632b5d76004d88ef0fbf444984887b121dece6fa69f4172ed4b2b07 + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -9956,6 +9974,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c + languageName: node + linkType: hard + "@types/unist@npm:^2.0.0": version: 2.0.11 resolution: "@types/unist@npm:2.0.11" @@ -11981,7 +12006,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.4, content-type@npm:^1.0.5": +"content-type@npm:^1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10c0/b76ebed15c000aee4678c3707e0860cb6abd4e680a598c0a26e17f0bfae723ec9cc2802f0ff1bc6e4d80603719010431d2231018373d4dde10f9ccff9dadf5af @@ -12440,6 +12465,18 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:*, dompurify@npm:^3.4.13": + version: 3.4.13 + resolution: "dompurify@npm:3.4.13" + dependencies: + "@types/trusted-types": "npm:^2.0.7" + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: 10c0/9c2a1a71e1a1d8b77953db7a39ffc935ef4ed4f10fe40770232128c2cbfd4c3dc677d3d7bae51eb24cc52d9ff3548612dc540ecb4343e89b26e809d2be2e0cfe + languageName: node + linkType: hard + "dotenv-cli@npm:^11.0.0": version: 11.0.0 resolution: "dotenv-cli@npm:11.0.0" @@ -14728,10 +14765,10 @@ __metadata: languageName: node linkType: hard -"is-network-error@npm:^1.1.0": - version: 1.3.1 - resolution: "is-network-error@npm:1.3.1" - checksum: 10c0/389b4a4cc6838bc5764c1d4ab8af11ec68c63825d53f7ce9f5a31aa4d2c9e5d33896c052f4c44100911e8db47bcf854c4aae6c03d6b1d84700f7c6aa72d16693 +"is-network-error@npm:^1.3.0": + version: 1.3.2 + resolution: "is-network-error@npm:1.3.2" + checksum: 10c0/37edc576497b21d022754b49203358ee80fdac4a11a71a09687f38ad789ec437dd930c223301df39f1c920566692b31345e0108ae720996e592cab3879eca74f languageName: node linkType: hard @@ -15692,13 +15729,6 @@ __metadata: languageName: node linkType: hard -"jwt-decode@npm:^4.0.0": - version: 4.0.0 - resolution: "jwt-decode@npm:4.0.0" - checksum: 10c0/de75bbf89220746c388cf6a7b71e56080437b77d2edb29bae1c2155048b02c6b8c59a3e5e8d6ccdfd54f0b8bda25226e491a4f1b55ac5f8da04cfbadec4e546c - languageName: node - linkType: hard - "katex@npm:^0.16.0": version: 0.16.45 resolution: "katex@npm:0.16.45" @@ -16238,35 +16268,32 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@npm:^39.4.0": - version: 39.4.0 - resolution: "matrix-js-sdk@npm:39.4.0" +"matrix-js-sdk@npm:^42.1.0": + version: 42.1.0 + resolution: "matrix-js-sdk@npm:42.1.0" dependencies: - "@babel/runtime": "npm:^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.3.0" + "@babel/runtime": "npm:^8.0.0" + "@matrix-org/matrix-sdk-crypto-wasm": "npm:^18.4.0" another-json: "npm:^0.2.0" bs58: "npm:^6.0.0" - content-type: "npm:^1.0.4" - jwt-decode: "npm:^4.0.0" + content-type: "npm:^2.0.0" loglevel: "npm:^1.9.2" matrix-events-sdk: "npm:0.0.1" - matrix-widget-api: "npm:^1.14.0" - oidc-client-ts: "npm:^3.0.1" - p-retry: "npm:7" + matrix-widget-api: "npm:^1.16.1" + p-retry: "npm:8" sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" - uuid: "npm:13" - checksum: 10c0/ab9450f49399fea23337e839ded956f1344b807f2a7c83f630c695054ca3e5c0e9123d7e9528678a99f8667ffdb701f0b35c8fd5f328ad4c449b14e276caba9e + checksum: 10c0/073a9aa927183772432c293bf9bff50a5f1b8b7048014d1e826e9a83d71a6da9873e79cfe6d0c43a6e3978ff8ac94883749b24d4e46247c11156f41ea5a60b4d languageName: node linkType: hard -"matrix-widget-api@npm:^1.14.0": - version: 1.17.0 - resolution: "matrix-widget-api@npm:1.17.0" +"matrix-widget-api@npm:^1.16.1": + version: 1.18.0 + resolution: "matrix-widget-api@npm:1.18.0" dependencies: "@types/events": "npm:^3.0.0" events: "npm:^3.2.0" - checksum: 10c0/3651c860900149ecc2fe74640b47687bab8a347eb718a522085189e2b84efe462c9d81c1e8caff08d122f0b3e9cef8303a5802837673e5c9b465f7624c56a8f3 + checksum: 10c0/0d2519a37d6b63f26c0ba51640b44519256a2067ef945c2fd31a8083d144ef02fd9328d27ba0be21ff9358c35e09d5941ed4fa18a4d82b6817e302bb8cd6ef5f languageName: node linkType: hard @@ -17240,15 +17267,6 @@ __metadata: languageName: node linkType: hard -"oidc-client-ts@npm:^3.0.1": - version: 3.5.0 - resolution: "oidc-client-ts@npm:3.5.0" - dependencies: - jwt-decode: "npm:^4.0.0" - checksum: 10c0/93cbb62adbfeee4bf705199a94e5790217f211b85dd988827e555481d4e605a5263292b740f29765ac01528b99486f57bbd96679ffca53d7ef76d8f7f7c9f979 - languageName: node - linkType: hard - "on-finished@npm:^2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -17411,12 +17429,12 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:7": - version: 7.1.1 - resolution: "p-retry@npm:7.1.1" +"p-retry@npm:8": + version: 8.0.0 + resolution: "p-retry@npm:8.0.0" dependencies: - is-network-error: "npm:^1.1.0" - checksum: 10c0/d72fb15dace25b8bf72c97a13c8a630ad1deb4667e708955e8806ee38f1d70e9611598ebe57bd9677349256e024a5599292f99aabb203143e0e13f1735e30818 + is-network-error: "npm:^1.3.0" + checksum: 10c0/81a788f35888c3cdb36f97286577c8ba57ccd8e9347db3c6ded79b3e12e2838bcda733753e0c0bdaac77d4620ddfc7e230d3c9a4bcf2fda3a12ea57bcf9443d1 languageName: node linkType: hard @@ -20904,15 +20922,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:13": - version: 13.0.0 - resolution: "uuid@npm:13.0.0" - bin: - uuid: dist-node/bin/uuid - checksum: 10c0/950e4c18d57fef6c69675344f5700a08af21e26b9eff2bf2180427564297368c538ea11ac9fb2e6528b17fc3966a9fd2c5049361b0b63c7d654f3c550c9b3d67 - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" -- GitLab From 1ae29552d495931a585045bdc07a78a9904190c7 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 7 Aug 2026 00:26:43 -0600 Subject: [PATCH 2/3] fix read receipts, reactions & load on scroll --- .../lib/components/ChatLog/ChatBubble.tsx | 139 +++++++------- .../lib/components/ChatLog/ChatTimeline.tsx | 180 ++++++++++++++---- .../chat/lib/components/ChatLog/Reactions.tsx | 19 ++ .../lib/components/ChatLog/ReadReceipts.tsx | 2 +- packages/chat/lib/hooks/useRead.ts | 36 ++-- packages/chat/lib/hooks/useTimeline.ts | 23 ++- packages/chat/lib/lib/getReceiptsForEvent.ts | 47 +++++ packages/chat/lib/lib/isVisibleEvent.ts | 9 + packages/chat/package.json | 1 + yarn.lock | 14 ++ 10 files changed, 339 insertions(+), 131 deletions(-) create mode 100644 packages/chat/lib/components/ChatLog/Reactions.tsx create mode 100644 packages/chat/lib/lib/getReceiptsForEvent.ts create mode 100644 packages/chat/lib/lib/isVisibleEvent.ts diff --git a/packages/chat/lib/components/ChatLog/ChatBubble.tsx b/packages/chat/lib/components/ChatLog/ChatBubble.tsx index 78efe34..b01ac96 100644 --- a/packages/chat/lib/components/ChatLog/ChatBubble.tsx +++ b/packages/chat/lib/components/ChatLog/ChatBubble.tsx @@ -1,85 +1,82 @@ -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( - ({ - room, - event, - isLatest, - }: { - room: sdk.Room; - event: sdk.MatrixEvent; - isLatest: boolean; - }) => { - 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()); +export const ChatBubble = ({ + room, + event, + isLatest, +}: { + room: sdk.Room; + event: sdk.MatrixEvent; + isLatest: boolean; +}) => { + const { client } = useYap<"Internal">(); + const isRight = event.getSender() === client.client.getUserId(); + const [edited, setEdited] = useState(!!event.replacingEvent()); - useEffect(() => { - const update = () => { - setContent(event.getContent()); - setEdited(!!event.replacingEvent()); - }; + useEffect(() => { + const update = () => { + setEdited(!!event.replacingEvent()); + }; - event.on(sdk.MatrixEventEvent.Decrypted, update); - event.on(sdk.MatrixEventEvent.Replaced, update); + update(); - return () => { - event.off(sdk.MatrixEventEvent.Decrypted, update); - event.off(sdk.MatrixEventEvent.Replaced, update); - }; - }, [event]); + event.on(sdk.MatrixEventEvent.Decrypted, update); + event.on(sdk.MatrixEventEvent.Replaced, update); - return ( -
- {/* {role === "SYSTEM" ? ( + return () => { + event.off(sdk.MatrixEventEvent.Decrypted, update); + event.off(sdk.MatrixEventEvent.Replaced, update); + }; + }, [event, client]); + + return ( +
+ {/* {role === "SYSTEM" ? (
{event.content}
) : ( */} - <> -
- - {event.getSender() && room.getMember(event.getSender()!)?.name} - -
-
+
+ + {event.getSender() && room.getMember(event.getSender()!)?.name} + +
+
+

-

- -

-
-
- {edited && edited} - {event.isSending() && sending...} - {isLatest && ( - - )} -
- - {/* )} */} -
- ); - }, -); + +

+
+
+ {edited && edited} + {event.isSending() && sending...} + {isLatest && ( + + )} + +
+ + {/* )} */} +
+ ); +}; diff --git a/packages/chat/lib/components/ChatLog/ChatTimeline.tsx b/packages/chat/lib/components/ChatLog/ChatTimeline.tsx index 96a0d09..801119f 100644 --- a/packages/chat/lib/components/ChatLog/ChatTimeline.tsx +++ b/packages/chat/lib/components/ChatLog/ChatTimeline.tsx @@ -1,9 +1,29 @@ 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,47 +32,137 @@ export const ChatTimeline = ({ client: MXClient; room: sdk.Room; }) => { + const top = useOnInView((inView) => { + if (inView) { + backward(); + } + }); const bottom = useRef(null); - const { events, forward, backward, loading } = useTimeline(client, room); + 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 }; - const scrollBottom = useCallback(() => { - bottom.current?.scrollIntoView({ behavior: "smooth" }); - }, []); + 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; - useEffect(() => { - scrollBottom(); - }, [events, scrollBottom]); + 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 ( -
    -
  • backward()}>top {loading.backward && "loading"}
  • - {events - .filter( - (ev) => - !ev.isRelation("m.replace") && - !ev.isRelation("m.annotation") && - !ev.isRedaction() && - !ev.isRedacted(), - ) - .map((ev, i, arr) => ( -
  • console.log(ev)}> - {ev.getType()} - {ev.getType() === "m.room.message" && ( - - )} -
  • - ))} -
  • forward()} ref={bottom}> - bottom {loading.forward && "loading"} -
  • -
+ +
    +
  • backward()}> + top {loading.backward && "loading"} {more.backward && "has more"} +
  • + {events + .filter( + (ev) => + !ev.isRelation("m.replace") && + !ev.isRelation("m.annotation") && + !ev.isRedaction() && + !ev.isRedacted(), + ) + .map((ev, i, arr) => ( +
  • console.log(ev)}> + {ev.getType()} + {ev.getType() === "m.room.message" && ( + + )} +
  • + ))} +
  • forward()} ref={bottom}> + bottom {loading.forward && "loading"} {more.forward && "has more"} +
  • +
+
); }; diff --git a/packages/chat/lib/components/ChatLog/Reactions.tsx b/packages/chat/lib/components/ChatLog/Reactions.tsx new file mode 100644 index 0000000..7ca638c --- /dev/null +++ b/packages/chat/lib/components/ChatLog/Reactions.tsx @@ -0,0 +1,19 @@ +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(",")} + ); +}; diff --git a/packages/chat/lib/components/ChatLog/ReadReceipts.tsx b/packages/chat/lib/components/ChatLog/ReadReceipts.tsx index c4b332d..ab10b59 100644 --- a/packages/chat/lib/components/ChatLog/ReadReceipts.tsx +++ b/packages/chat/lib/components/ChatLog/ReadReceipts.tsx @@ -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}); }; diff --git a/packages/chat/lib/hooks/useRead.ts b/packages/chat/lib/hooks/useRead.ts index 0da821c..86d36f5 100644 --- a/packages/chat/lib/hooks/useRead.ts +++ b/packages/chat/lib/hooks/useRead.ts @@ -1,6 +1,7 @@ +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([]); - 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)); - - setReadBy(members); - }; + 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; - - update(); + 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; }; diff --git a/packages/chat/lib/hooks/useTimeline.ts b/packages/chat/lib/hooks/useTimeline.ts index d8d66b2..4ee361b 100644 --- a/packages/chat/lib/hooks/useTimeline.ts +++ b/packages/chat/lib/hooks/useTimeline.ts @@ -2,7 +2,11 @@ import type { MXClient } from "@/lib/MXClient"; import * as sdk from "matrix-js-sdk"; import { useCallback, useEffect, useState, useReducer } from "react"; -export function useTimeline(client: MXClient, room: sdk.Room) { +export function useTimeline( + client: MXClient, + room: sdk.Room, + onEvent: (event: sdk.MatrixEvent) => unknown, +) { const [timelineWindow, setTimelineWindow] = useState(); const [events, setEvents] = useState([]); const [loading, dLoading] = useReducer( @@ -15,6 +19,14 @@ export function useTimeline(client: MXClient, room: sdk.Room) { forward: false, }, ); + const [more, setMore] = useState({ backward: false, forward: false }); + + const updateMore = useCallback(() => { + setMore({ + backward: !!timelineWindow?.canPaginate(sdk.EventTimeline.BACKWARDS), + forward: !!timelineWindow?.canPaginate(sdk.EventTimeline.FORWARDS), + }); + }, [timelineWindow]); const sync = useCallback(() => { if (timelineWindow) { @@ -28,7 +40,7 @@ export function useTimeline(client: MXClient, room: sdk.Room) { room!.getUnfilteredTimelineSet()!, ); - tlwindow.load(undefined, 20).then(() => { + tlwindow.load(undefined, 50).then(() => { setEvents([...tlwindow.getEvents()]); }); @@ -51,11 +63,14 @@ export function useTimeline(client: MXClient, room: sdk.Room) { if (timelineWindow.canPaginate(sdk.EventTimeline.FORWARDS)) { timelineWindow.paginate(sdk.EventTimeline.FORWARDS, 10).then(() => { setEvents([...timelineWindow.getEvents()]); + updateMore(); }); } else { setEvents([...timelineWindow.getEvents()]); } } + + onEvent?.(event); }; client.client.on(sdk.RoomEvent.Timeline, handleTimeline); @@ -78,6 +93,7 @@ export function useTimeline(client: MXClient, room: sdk.Room) { .paginate(sdk.EventTimeline.FORWARDS, 20) .finally(() => { dLoading(["end", "forward"]); + updateMore(); }); sync(); } @@ -90,10 +106,11 @@ export function useTimeline(client: MXClient, room: sdk.Room) { .paginate(sdk.EventTimeline.BACKWARDS, 20) .finally(() => { dLoading(["end", "backward"]); + updateMore(); }); sync(); } }; - return { events, forward, backward, loading }; + return { events, forward, backward, loading, more }; } diff --git a/packages/chat/lib/lib/getReceiptsForEvent.ts b/packages/chat/lib/lib/getReceiptsForEvent.ts new file mode 100644 index 0000000..7bc2f9c --- /dev/null +++ b/packages/chat/lib/lib/getReceiptsForEvent.ts @@ -0,0 +1,47 @@ +import * as sdk from "matrix-js-sdk"; +import type { MXUserID } from "./const"; +import { isVisibleEvent } from "./isVisibleEvent"; + +export const getReceiptsForEvent = (room: sdk.Room, event: sdk.MatrixEvent) => { + const eventId = event.getId(); + if (!eventId) return []; + + const userIds = new Set(); + const events = room.getLiveTimeline().getEvents(); + const index = events.findIndex((e) => e.getId() === eventId); + + if (index !== -1) { + for (let i = index; i < events.length; i++) { + const currentEvent = events[i]; + + if (i > index && isVisibleEvent(currentEvent)) break; + + const receipts = room.getReceiptsForEvent(currentEvent) || []; + for (const receipt of receipts) { + if (receipt.type === "m.read") { + userIds.add(receipt.userId as MXUserID); + } + } + } + } else { + const receipts = room.getReceiptsForEvent(event) || []; + for (const receipt of receipts) { + if (receipt.type === "m.read") { + userIds.add(receipt.userId as MXUserID); + } + } + } + + const joinedMembers = room.getJoinedMembers(); + for (const member of joinedMembers) { + if (!userIds.has(member.userId as MXUserID)) { + if (room.hasUserReadEvent(member.userId, eventId)) { + userIds.add(member.userId as MXUserID); + } + } + } + + return Array.from(userIds) + .map((userId) => room.getMember(userId)) + .filter((member): member is sdk.RoomMember => member !== null); +}; diff --git a/packages/chat/lib/lib/isVisibleEvent.ts b/packages/chat/lib/lib/isVisibleEvent.ts new file mode 100644 index 0000000..97de27a --- /dev/null +++ b/packages/chat/lib/lib/isVisibleEvent.ts @@ -0,0 +1,9 @@ +import * as sdk from "matrix-js-sdk"; + +export const isVisibleEvent = (event: sdk.MatrixEvent) => { + if (event.getType() === sdk.EventType.Reaction) return false; + if (event.getType() === sdk.EventType.RoomRedaction) return false; + if (event.isState()) return false; + + return true; +}; diff --git a/packages/chat/package.json b/packages/chat/package.json index 2f15ebc..8a66064 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -32,6 +32,7 @@ "matrix-js-sdk": "^42.1.0", "react": "^19.2.7", "react-dom": "^19.2.7", + "react-intersection-observer": "^11.0.0", "tailwindcss": "^4.3.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 7ffa354..6d6956d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8540,6 +8540,7 @@ __metadata: matrix-js-sdk: "npm:^42.1.0" react: "npm:^19.2.7" react-dom: "npm:^19.2.7" + react-intersection-observer: "npm:^11.0.0" tailwindcss: "npm:^4.3.2" typescript: "npm:~5.9.3" typescript-eslint: "npm:^8.64.0" @@ -18169,6 +18170,19 @@ __metadata: languageName: node linkType: hard +"react-intersection-observer@npm:^11.0.0": + version: 11.0.0 + resolution: "react-intersection-observer@npm:11.0.0" + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react-dom: + optional: true + checksum: 10c0/cecfdc1b2d939749d6fbc27ac0957515529bff5d0fbba751bb6b4473961636d9bb7f663e8d7d8afff476e06ac8d6627fd19bc022320916d286aae96017ea57e8 + languageName: node + linkType: hard + "react-is-18@npm:react-is@^18.3.1, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" -- GitLab From 1e343d49c1c9e2890e1661e22ea37442a15f0538 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 7 Aug 2026 15:23:52 -0600 Subject: [PATCH 3/3] fix importing to allow for service worker to be imported (sw is not implemented) --- packages/chat/eslint.config.js | 4 ++++ packages/chat/lib/sw.ts | 24 ++++++++++++++++++++++++ packages/chat/package.json | 3 +++ packages/chat/src/main.tsx | 29 +++++++++++++++++++++++------ packages/chat/tsconfig.json | 3 ++- packages/chat/tsconfig.lib.json | 1 + packages/chat/tsconfig.sw.json | 10 ++++++++++ packages/chat/vite.config.ts | 10 ++++++---- 8 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 packages/chat/lib/sw.ts create mode 100644 packages/chat/tsconfig.sw.json diff --git a/packages/chat/eslint.config.js b/packages/chat/eslint.config.js index ac01f6b..b509a14 100644 --- a/packages/chat/eslint.config.js +++ b/packages/chat/eslint.config.js @@ -31,6 +31,10 @@ export default defineConfig([ }, ], languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, ecmaVersion: 2020, globals: globals.browser, }, diff --git a/packages/chat/lib/sw.ts b/packages/chat/lib/sw.ts new file mode 100644 index 0000000..a419dc5 --- /dev/null +++ b/packages/chat/lib/sw.ts @@ -0,0 +1,24 @@ +const swSelf = self as unknown as ServiceWorkerGlobalScope; + +swSelf.addEventListener("install", (event) => { + event.waitUntil(swSelf.skipWaiting()); +}); + +swSelf.addEventListener("activate", (event) => { + event.waitUntil(swSelf.clients.claim()); +}); + +swSelf.addEventListener("fetch", (event: FetchEvent) => { + const newRequest = new Request(event.request, { + headers: { + Authorization: `*** abc`, + }, + mode: "cors", + }); + + postMessage("hello!"); + + event.respondWith(fetch(newRequest)); +}); + +console.log("servidce worker online"); diff --git a/packages/chat/package.json b/packages/chat/package.json index 8a66064..f91b167 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -13,6 +13,9 @@ "./Yapper": { "default": "./dist/Yapper.js", "types": "./dist/components/Yapper.d.ts" + }, + "./sw": { + "default": "./dist/sw.js" } }, "files": [ diff --git a/packages/chat/src/main.tsx b/packages/chat/src/main.tsx index bef5202..3354897 100644 --- a/packages/chat/src/main.tsx +++ b/packages/chat/src/main.tsx @@ -1,10 +1,27 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; +import swUrl from "@sc07-canvas/chat/sw?url"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , -) +); + +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register(swUrl).then((register) => { + navigator.serviceWorker.addEventListener("message", (ev) => { + console.log("SERVICE WORKER:", ev); + }); + navigator.serviceWorker.addEventListener("messageerror", (ev) => { + console.error("SERVICE WORKER:", ev); + }); + register.update().then(() => { + console.log("serviceWorker update"); + }); + }); + }); +} diff --git a/packages/chat/tsconfig.json b/packages/chat/tsconfig.json index b6a01bf..5b174d0 100644 --- a/packages/chat/tsconfig.json +++ b/packages/chat/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }, - { "path": "./tsconfig.lib.json" } + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.sw.json" } ] } diff --git a/packages/chat/tsconfig.lib.json b/packages/chat/tsconfig.lib.json index 4d97e69..e65db49 100644 --- a/packages/chat/tsconfig.lib.json +++ b/packages/chat/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.app.json", "include": ["lib/**/*"], + "exclude": ["lib/sw.ts"], "compilerOptions": { "paths": { "@/*": ["./lib/*"] diff --git a/packages/chat/tsconfig.sw.json b/packages/chat/tsconfig.sw.json new file mode 100644 index 0000000..eedd542 --- /dev/null +++ b/packages/chat/tsconfig.sw.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.app.json", + "include": ["lib/sw.ts"], + "compilerOptions": { + "lib": ["ES2022", "WebWorker"], + "types": [], + "paths": {}, + "moduleDetection": "legacy" + } +} \ No newline at end of file diff --git a/packages/chat/vite.config.ts b/packages/chat/vite.config.ts index dc57d68..df2d75f 100644 --- a/packages/chat/vite.config.ts +++ b/packages/chat/vite.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ entry: [ resolve(__dirname, "lib/main.ts"), resolve(__dirname, "lib/components/Yapper.tsx"), + resolve(__dirname, "lib/sw.ts"), ], formats: ["es"], }, @@ -28,10 +29,11 @@ export default defineConfig({ }, }, resolve: { - alias: { - "@sc07-canvas/chat": resolve(__dirname, "lib/main.ts"), - "@": resolve(__dirname, "lib"), - }, + alias: [ + { find: /^@sc07-canvas\/chat\/sw(\?.*)?$/, replacement: resolve(__dirname, "lib/sw.ts$1") }, + { find: /^@sc07-canvas\/chat$/, replacement: resolve(__dirname, "lib/main.ts") }, + { find: /^@\//, replacement: resolve(__dirname, "lib") + "/" }, + ], }, define: { __HOMESERVER__: JSON.stringify( -- GitLab