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"