diff --git a/packages/chat/eslint.config.js b/packages/chat/eslint.config.js index ac01f6b658e0b726f8958e542a5cc7e8d9de9e66..b509a14bb3be6a3d2f283ec7d12ca4af7327a926 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/components/ChatLog/ChatBubble.tsx b/packages/chat/lib/components/ChatLog/ChatBubble.tsx index eeece640e202f1db099493f9771698871fd2bdd4..b01ac969a099521cdc810ee544e04e93cc23f818 100644 --- a/packages/chat/lib/components/ChatLog/ChatBubble.tsx +++ b/packages/chat/lib/components/ChatLog/ChatBubble.tsx @@ -1,9 +1,38 @@ +import { 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"; +import { Reactions } from "./Reactions"; -export const ChatBubble = ({ event }: { event: MatrixEvent }) => { +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 = () => { + setEdited(!!event.replacingEvent()); + }; + + update(); + + 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, client]); return (
{ ) : ( */} <>
- {event.getSender()} + + {event.getSender() && room.getMember(event.getSender()!)?.name} +
- {event.getContent().body} +

+ +

- {/* {flags & MessageFlag.SENDING ? sending : ""} - {flags & MessageFlag.SEEN ? seen : ""} - {flags & MessageFlag.EDITED ? edited : ""} */} + {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 8363e955635212f763c6ca826fcec5e83b2ff27e..801119fcb5d9187aac2a6f7dc7a896c810e2800d 100644 --- a/packages/chat/lib/components/ChatLog/ChatTimeline.tsx +++ b/packages/chat/lib/components/ChatLog/ChatTimeline.tsx @@ -1,68 +1,168 @@ 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 { useOnInView } from "react-intersection-observer"; +import type { MXEventID, MXUserID } from "@/main"; +import * as sdk from "matrix-js-sdk"; +import { createContext, useEffect, useReducer, useRef } from "react"; import { ChatBubble } from "./ChatBubble"; +import { useTimeline } from "@/hooks/useTimeline"; +import { isVisibleEvent } from "@/lib/isVisibleEvent"; -export const ChatTimeline = ({ roomId }: { roomId: MXRoomID }) => { - const timelineRef = useRef(null); - const [events, setEvents] = useState([]); +export const RoomContext = createContext<{ + reactions: { + [eventId: MXEventID]: { + [emoji: string]: { [userId: MXUserID]: MXEventID }; + }; + }; +}>({ reactions: {} }); - function getTimeline() { - if (timelineRef.current !== null) { - return timelineRef.current; +// 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, + room, +}: { + client: MXClient; + room: sdk.Room; +}) => { + const top = useOnInView((inView) => { + if (inView) { + backward(); } + }); + const bottom = useRef(null); + 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 }; + + 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; + + 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; - const { client, rooms } = MXClient.get(); - const room = rooms.getRoom(roomId); + state[event_id][emoji][sender] = event.getId()! as MXEventID; + } + } - return (timelineRef.current = new TimelineWindow( - client, - room!.getUnfilteredTimelineSet()!, - )); - } + return state; + }, + {} as { + [eventId: MXEventID]: { + [emoji: string]: { [userId: MXUserID]: MXEventID }; + }; + }, + ); useEffect(() => { - const { client } = MXClient.get(); - - getTimeline() - .load() - .then(() => { - setEvents(getTimeline().getEvents()); - }) - .catch((e) => { - console.error(e); - }); - - client.on(RoomEvent.Timeline, (event, room, toStartOfTimeline) => { - if (!room || room.roomId !== roomId) return; - if (toStartOfTimeline) { - console.log("tostartoftimeline"); - return; + 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]]); } + }; - getTimeline() - .paginate(EventTimeline.FORWARDS, 1, false) - .then(() => { - setEvents(getTimeline().getEvents()); - }); - }); - }, [roomId]); + 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 ( -
    - {events.map((ev) => ( -
  • - + +
      +
    • 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/MessageBody.tsx b/packages/chat/lib/components/ChatLog/MessageBody.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7bc32555712fb7c58bb89c084df1553b0f982e3f --- /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/Reactions.tsx b/packages/chat/lib/components/ChatLog/Reactions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7ca638c903471ad3beb59707c4f4e6cd616cb35b --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..ab10b594d11e178a96ebd9365d6efc9f482966a0 --- /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 68532192fb69ff05f6142d7f45c0bd7497505879..8fb13c8262c105e56e17c2dee4071999a07115d5 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 7c48731363b43c999f9604e350d741adbf6cf49e..00bc38a10f65b87530a95ef8e57a4aece5e74af9 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 0000000000000000000000000000000000000000..86d36f582da175ee0a8d47eab6e2798582d3a756 --- /dev/null +++ b/packages/chat/lib/hooks/useRead.ts @@ -0,0 +1,35 @@ +import { getReceiptsForEvent } from "@/lib/getReceiptsForEvent"; +import type { MXClient } from "@/lib/MXClient"; +import * as sdk from "matrix-js-sdk"; +import { useCallback, useEffect, useState } from "react"; + +export const useRead = ( + client: MXClient, + room: sdk.Room, + event: sdk.MatrixEvent, +) => { + const [readBy, setReadBy] = useState([]); + + const update = useCallback(() => { + const members = getReceiptsForEvent(room, event); + setReadBy(members); + }, [room, event]); + + useEffect(() => { + update(); + + const handle = (_event: sdk.MatrixEvent, rroom?: sdk.Room) => { + if (rroom?.roomId === room.roomId) { + update(); + } + }; + + room.client.on(sdk.RoomEvent.Receipt, handle); + + return () => { + room.client.off(sdk.RoomEvent.Receipt, handle); + }; + }, [room, event, update]); + + return readBy; +}; diff --git a/packages/chat/lib/hooks/useRoom.ts b/packages/chat/lib/hooks/useRoom.ts index dab9169980f1b1373a2866e608dc466c4197ccdf..2f90abfff6d1dec3a2ebac281c95b113baecaf06 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 0000000000000000000000000000000000000000..4ee361b68660a25128b937825bf7c515a6036f53 --- /dev/null +++ b/packages/chat/lib/hooks/useTimeline.ts @@ -0,0 +1,116 @@ +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, + onEvent: (event: sdk.MatrixEvent) => unknown, +) { + 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 [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) { + setEvents([...timelineWindow.getEvents()]); + } + }, [timelineWindow]); + + useEffect(() => { + const tlwindow = new sdk.TimelineWindow( + client.client, + room!.getUnfilteredTimelineSet()!, + ); + + tlwindow.load(undefined, 50).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()]); + updateMore(); + }); + } else { + setEvents([...timelineWindow.getEvents()]); + } + } + + onEvent?.(event); + }; + + 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"]); + updateMore(); + }); + sync(); + } + }; + + const backward = async () => { + if (timelineWindow?.canPaginate(sdk.EventTimeline.BACKWARDS)) { + dLoading(["start", "backward"]); + await timelineWindow + .paginate(sdk.EventTimeline.BACKWARDS, 20) + .finally(() => { + dLoading(["end", "backward"]); + updateMore(); + }); + sync(); + } + }; + + return { events, forward, backward, loading, more }; +} diff --git a/packages/chat/lib/lib/MXClient.ts b/packages/chat/lib/lib/MXClient.ts index 6685423fa7e1194c14321503951ff63f3dd9748a..957b3bff959fc43348b44c51be8789ac539a6bd2 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 3e36637968317fbcd1e2a1d97ca59dc0ba5d9e48..f6b4e3a341165f9aaadc1c36df544a33587efa70 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/lib/getReceiptsForEvent.ts b/packages/chat/lib/lib/getReceiptsForEvent.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bc2f9c1a0b9c3f31f54a765b5146d42f8fa9593 --- /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 0000000000000000000000000000000000000000..97de27a43f8c662be8e128234d37039b9d7940d6 --- /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/lib/main.ts b/packages/chat/lib/main.ts index 7690a8a41523cf980cf6647097100f9b5199c437..7357602a9352d51ca7a2cca86ea842f9c0ff9d54 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/lib/sw.ts b/packages/chat/lib/sw.ts new file mode 100644 index 0000000000000000000000000000000000000000..a419dc57b380f63f9527d9e2cb0d3ee490cac13e --- /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 74b07c02f3f28dd9416e0a77026945be0dd5a8ad..f91b16783fe798ff134bf68801acccbbf8433341 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": [ @@ -26,15 +29,18 @@ }, "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", + "react-intersection-observer": "^11.0.0", "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/packages/chat/src/main.tsx b/packages/chat/src/main.tsx index bef5202a32cbd0632c43de40f6e908532903fd42..3354897ebd568b8709848835800b2c875b128ef1 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 b6a01bf3b76ac27ef1dde0eb8d0fc9931aaaf34e..5b174d02cf1c6fffdee01e83627339ebc20f3339 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 4d97e6998121312dfdc0d20661c40cb0fd062e2e..e65db49f2920a0d5d57f17fd1a65a5304d80c2f8 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 0000000000000000000000000000000000000000..eedd5427456e3650c58aad77fc1e852ed0fa5dce --- /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 dc57d6871630e7ee5b335a108b7140c6748a8c4e..df2d75f36597c1135c881ac56866b81d01b3bf57 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( diff --git a/yarn.lock b/yarn.lock index edfb32a6ca512bc438e4c807e228a76092dff0fb..6d6956d4a2ffa2a010d60cdd528b184547f2eab8 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,20 +8524,23 @@ __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" + react-intersection-observer: "npm:^11.0.0" tailwindcss: "npm:^4.3.2" typescript: "npm:~5.9.3" typescript-eslint: "npm:^8.64.0" @@ -9644,6 +9654,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 +9975,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 +12007,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 +12466,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 +14766,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 +15730,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 +16269,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 +17268,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 +17430,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 @@ -18151,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" @@ -20904,15 +20936,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"