From 2eaa8a9fa491a80e45438c79d9101a820cd0fbd2 Mon Sep 17 00:00:00 2001 From: toshanmugaraj Date: Mon, 10 Feb 2025 08:18:41 +0300 Subject: [PATCH 1/3] Add annotation to the map. Encrypt the annotations and store in the room state. Use the pinned message as the password to encrypt and decrypt. Delete annotation in the map. Choose annotation color and description while marking. --- .../components/views/location/_Marker.pcss | 63 ++++++ src/components/views/location/Annotation.tsx | 7 + .../views/location/AnnotationDialog.tsx | 70 +++++++ .../views/location/AnnotationMarker.tsx | 78 ++++++++ .../views/location/AnnotationPin.ts | 188 ++++++++++++++++++ .../views/location/AnnotationSmartMarker.tsx | 90 +++++++++ src/components/views/location/Map.tsx | 129 +++++++++++- 7 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 src/components/views/location/Annotation.tsx create mode 100644 src/components/views/location/AnnotationDialog.tsx create mode 100644 src/components/views/location/AnnotationMarker.tsx create mode 100644 src/components/views/location/AnnotationPin.ts create mode 100644 src/components/views/location/AnnotationSmartMarker.tsx diff --git a/res/css/components/views/location/_Marker.pcss b/res/css/components/views/location/_Marker.pcss index 5a8fef91992..de2244487e9 100644 --- a/res/css/components/views/location/_Marker.pcss +++ b/res/css/components/views/location/_Marker.pcss @@ -10,6 +10,69 @@ Please see LICENSE files in the repository root for full details. color: $accent; } +.mx_Marker_red { + color: red; +} + +.mx_Marker_green { + color: green; +} + +.mx_Marker_blue { + color: blue; +} + +.mx_Marker_yellow { + color: yellow; +} + +.mx_Marker_cyan { + color: cyan; +} + +.mx_Marker_magenta { + color: magenta; +} + +.mx_Marker_orange { + color: orange; +} + +.mx_Marker_purple { + color: purple; +} + +.mx_Marker_pink { + color: pink; +} + +.mx_Marker_brown { + color: brown; +} + +.mx_AnnotationMarker_border { + width: 22px; + height: 22px; + border-radius: 50%; + filter: drop-shadow(0px 3px 5px rgba(0, 0, 0, 0.2)); + background-color: currentColor; + + display: flex; + justify-content: center; + align-items: center; + + /* caret down */ + &::before { + content: ""; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid currentColor; + position: absolute; + bottom: -4px; + } +} + + .mx_Marker_border { width: 42px; height: 42px; diff --git a/src/components/views/location/Annotation.tsx b/src/components/views/location/Annotation.tsx new file mode 100644 index 00000000000..42bc9a5fbf8 --- /dev/null +++ b/src/components/views/location/Annotation.tsx @@ -0,0 +1,7 @@ +interface Annotation { + body: string, + geoUri: string, + color?: string, +} + +export default Annotation; \ No newline at end of file diff --git a/src/components/views/location/AnnotationDialog.tsx b/src/components/views/location/AnnotationDialog.tsx new file mode 100644 index 00000000000..a20fc75dd4c --- /dev/null +++ b/src/components/views/location/AnnotationDialog.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import BaseDialog from "../dialogs/BaseDialog"; + + +interface Props { + onFinished: () => void; + onSubmit: (title: string, color: string) => void; + displayBack?: boolean; +} + +const colorOptions = [ + { label: 'Red', value: 'red' }, + { label: 'Green', value: 'green' }, + { label: 'Blue', value: 'blue' }, + { label: 'Yellow', value: 'yellow' }, + { label: 'Cyan', value: 'cyan' }, + { label: 'Magenta', value: 'magenta' }, + { label: 'Orange', value: 'orange' }, + { label: 'Purple', value: 'purple' }, + { label: 'Pink', value: 'pink' }, + { label: 'Brown', value: 'brown' }, +]; + +const AnnotationDialog: React.FC = ({ onSubmit, onFinished }) => { + + const [title, setTitle] = useState(''); + const [color, setColor] = useState('red'); // Default color + + const handleSubmit = () => { + onSubmit(title, color); + onFinished(); + }; + + return ( + +
+ setTitle(e.target.value)} + placeholder="Enter title" + /> + +
+ + +
+ + +
+
+ ); +}; + +export default AnnotationDialog; \ No newline at end of file diff --git a/src/components/views/location/AnnotationMarker.tsx b/src/components/views/location/AnnotationMarker.tsx new file mode 100644 index 00000000000..7171bf70ca2 --- /dev/null +++ b/src/components/views/location/AnnotationMarker.tsx @@ -0,0 +1,78 @@ +import React, { ReactNode, useState } from "react"; +import classNames from "classnames"; +import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; + + + + +/** + * Wrap with tooltip handlers when + * tooltip is truthy + */ +const OptionalTooltip: React.FC<{ + tooltip?: React.ReactNode; + annotationKey: string; + children: React.ReactNode; + onDelete: (key: string) => void; // Optional delete function +}> = ({ tooltip, children, onDelete, annotationKey }) => { + const [isHovered, setIsHovered] = useState(false); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + style={{ position: "relative" }} // Set position for proper tooltip alignment + > + {tooltip && ( +
+ {tooltip} + {isHovered && ( + + )} +
+ )} + {children} +
+ ); +}; + +/** + * Generic location marker + */ + +interface Props { + id: string; + useColor?: string; + tooltip?: ReactNode; + onDelete: (annotationKey: string) => void; +} + +const AnnotationMarker = React.forwardRef(({ id, useColor, tooltip, onDelete}, ref) => { + return ( +
+ +
+ +
+
+
+ ); +}); + +export default AnnotationMarker; \ No newline at end of file diff --git a/src/components/views/location/AnnotationPin.ts b/src/components/views/location/AnnotationPin.ts new file mode 100644 index 00000000000..803e02ae48f --- /dev/null +++ b/src/components/views/location/AnnotationPin.ts @@ -0,0 +1,188 @@ +import { EventTimeline, EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; +import Annotation from "./Annotation"; +import { RoomAnnotationEventContent } from "matrix-js-sdk/src/@types/state_events"; + +import * as MegolmExportEncryptionExport from "../../../../src/utils/MegolmExportEncryption" + +export const fetchPinnedEvent = async (roomId: string, matrixClient: MatrixClient): Promise => { + try { + const room = matrixClient.getRoom(roomId); + if (!room) { + console.error("Room not found"); + return null; + } + const readPinsEventId = getPinnedEventIds(room)[0]; + if (!readPinsEventId) { + console.error("Read pins event ID not found"); + return null; + } + let localEvent = room.findEventById(readPinsEventId); + + // Decrypt if necessary + if (localEvent?.isEncrypted()) { + await matrixClient.decryptEventIfNeeded(localEvent, { emit: false }); + } + + if (localEvent) { + return localEvent; // Return the pinned event itself + } + return null; + } catch (err) { + logger.error(`Error looking up pinned event in room ${roomId}`); + logger.error(err); + } + return null; +}; + +function getPinnedEventIds(room?: Room): string[] { + const eventIds: string[] = + room + ?.getLiveTimeline() + .getState(EventTimeline.FORWARDS) + ?.getStateEvents(EventType.RoomPinnedEvents, "") + ?.getContent()?.pinned ?? []; + // Limit the number of pinned events to 100 + return eventIds.slice(0, 1); +} + +export async function extractAnnotationsPassphrase(roomId: string, matrixClient: MatrixClient): Promise { + try { + const pinEvent = await fetchPinnedEvent(roomId, matrixClient); + if(!pinEvent) { + return null; + } + + let rawContent: string = pinEvent.getContent()["body"]; + return rawContent; + + } catch (error) { + console.error("Error retrieving content from pinned event:", error); + return null; + } + return null; +} + + +// Function to update annotations on the server +export const sendAnnotations = async (roomId: string, content: RoomAnnotationEventContent, matrixClient: MatrixClient) => { + try { + await matrixClient.sendStateEvent(roomId, EventType.RoomAnnotation, content); + console.log("Annotations updated successfully!"); + } catch (error) { + console.error("Failed to update annotations:", error); + throw error; // Rethrow the error for handling in the calling component + } +}; + + +// Function to save a new annotation +// Function to save an annotation +export const saveAnnotation = async (roomId: string, matrixClient: MatrixClient, annotations: Annotation[]) => { + try { + + const base64EncryptedAnnotations = await encryptAnnotations(roomId, matrixClient, annotations); + if(!base64EncryptedAnnotations) { + return []; + } + const content = { + annotations: base64EncryptedAnnotations, // Use the base64 string + }; + + await sendAnnotations(roomId, content, matrixClient); + } catch (error) { + console.error("Failed to save annotation:", error); + // Handle the error appropriately (e.g., notify the user) + throw error; // Optionally rethrow the error for further handling + } +}; + +// Function to delete an annotation +export const deleteAnnotation = async (roomId: string, matrixClient: MatrixClient, geoUri: string, annotations: Annotation[]) => { + try { + // Prepare content for the server + const base64EncryptedAnnotations = await encryptAnnotations(roomId, matrixClient, annotations); + if(!base64EncryptedAnnotations) { + return []; + } + const content = { + annotations: base64EncryptedAnnotations, // Use the base64 string + }; + + await sendAnnotations(roomId, content, matrixClient); + } catch (error) { + console.error("Failed to delete annotation:", error); + // Handle the error appropriately (e.g., notify the user) + throw error; // Optionally rethrow the error for further handling + } +}; + +// Function to load annotations from the server +export const loadAnnotations = async (roomId: string, matrixClient: MatrixClient): Promise => { + try { + const room = matrixClient.getRoom(roomId); // Get the room object + const events = room?.currentState.getStateEvents(EventType.RoomAnnotation); + + if (!events || events.length === 0) { + return []; // Return an empty array if no events are found + } + const password = await extractAnnotationsPassphrase(roomId, matrixClient); + if(!password) { + return []; + } + const event = events[0]; + const content = event.getContent(); // Get content from the event + const encryptedAnnotations = content.annotations || []; + if (typeof encryptedAnnotations !== 'string') { + console.warn("Content is not a string. Returning early."); + return []; // or handle accordingly + } + const decryptedArray: Annotation[] = await decryptAnnotations(encryptedAnnotations, password); + if (Object.keys(content.annotations).length === 0) { + return []; + } + return decryptedArray.map((annotation: { geoUri: string; body: string; color?: string; }) => ({ + geoUri: annotation.geoUri, + body: annotation.body, + color: annotation.color, + })); + + } catch (error) { + console.error("Failed to load annotations:", error); + return []; // Return an empty array in case of an error + } +}; + +export const decryptAnnotations = async (encryptedAnnotations: string, password: string): Promise => { + try { + // Convert from base64 string to ArrayBuffer + const binaryString = atob(encryptedAnnotations); + const charCodeArray = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + charCodeArray[i] = binaryString.charCodeAt(i); + } + const arrayBuffer = charCodeArray.buffer; + + // Decrypt the annotations + const decryptedAnnotations = await MegolmExportEncryptionExport.decryptMegolmKeyFile(arrayBuffer, password); + return JSON.parse(decryptedAnnotations); // Convert decrypted string back to JSON + } catch (error) { + console.error("Decryption failed:", error); + throw error; // Rethrow the error for further handling + } +}; + +export const encryptAnnotations = async (roomId: string, matrixClient: MatrixClient, annotations: Annotation[]): Promise => { + const jsonAnnotations = JSON.stringify(annotations); + const password = await extractAnnotationsPassphrase(roomId, matrixClient); + + if (!password) { + return null; + } + + const encryptedAnnotations = await MegolmExportEncryptionExport.encryptMegolmKeyFile(jsonAnnotations, password, { kdf_rounds: 1000 }); + const encryptedAnnotationsArray = new Uint8Array(encryptedAnnotations); + const base64EncryptedAnnotations = btoa(String.fromCharCode(...encryptedAnnotationsArray)); + + return base64EncryptedAnnotations; // Return the base64 encoded encrypted annotations +} \ No newline at end of file diff --git a/src/components/views/location/AnnotationSmartMarker.tsx b/src/components/views/location/AnnotationSmartMarker.tsx new file mode 100644 index 00000000000..42fae926eac --- /dev/null +++ b/src/components/views/location/AnnotationSmartMarker.tsx @@ -0,0 +1,90 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2022 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import React, { ReactNode, useCallback, useEffect, useState } from "react"; +import * as maplibregl from "maplibre-gl"; + +import { parseGeoUri } from "../../../utils/location"; +import { createMarker } from "../../../utils/location/map"; +import AnnotationMarker from "./AnnotationMarker"; + +const useMapMarker = ( + map: maplibregl.Map, + geoUri: string, +): { marker?: maplibregl.Marker; onElementRef: (el: HTMLDivElement) => void } => { + const [marker, setMarker] = useState(); + + const onElementRef = useCallback( + (element: HTMLDivElement) => { + if (marker || !element) { + return; + } + const coords = parseGeoUri(geoUri); + if (coords) { + const newMarker = createMarker(coords, element); + newMarker.addTo(map); + setMarker(newMarker); + } + }, + [marker, geoUri, map], + ); + + useEffect(() => { + if (marker) { + const coords = parseGeoUri(geoUri); + if (coords) { + marker.setLngLat({ lon: coords.longitude, lat: coords.latitude }); + } + } + }, [marker, geoUri]); + + useEffect( + () => () => { + if (marker) { + marker.remove(); + } + }, + [marker], + ); + + return { + marker, + onElementRef, + }; +}; + +export interface AnnotationSmartMarkerProps { + map: maplibregl.Map; + geoUri: string; + id: string; + key: string; + useColor?: string; + tooltip?: ReactNode; + onDelete: (key: string) => void; +} + +/** + * Generic location marker + */ +const AnnotationSmartMarker: React.FC = ({ id, map, geoUri, useColor, tooltip, onDelete }) => { + const { onElementRef } = useMapMarker(map, geoUri); + + return ( + + + + ); +}; + +export default AnnotationSmartMarker; diff --git a/src/components/views/location/Map.tsx b/src/components/views/location/Map.tsx index 998dff1d157..dff8377e7d5 100644 --- a/src/components/views/location/Map.tsx +++ b/src/components/views/location/Map.tsx @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { type ReactNode, useContext, useEffect, useState } from "react"; +import React, { ReactNode, useContext, useEffect, useState } from "react"; import classNames from "classnames"; import * as maplibregl from "maplibre-gl"; -import { ClientEvent, type IClientWellKnown } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, IClientWellKnown } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -21,6 +21,11 @@ import { type Bounds } from "../../../utils/beacon/bounds"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import { _t } from "../../../languageHandler"; +import AnnotationSmartMarker from './AnnotationSmartMarker'; +import Annotation from './Annotation'; +import AnnotationDialog from "./AnnotationDialog"; +import { loadAnnotations, saveAnnotation, deleteAnnotation } from "../location/AnnotationPin"; +import { SdkContextClass } from "../../../contexts/SDKContext"; const useMapWithStyle = ({ id, @@ -131,6 +136,14 @@ const onGeolocateError = (e: GeolocationPositionError): void => { }); }; +interface ClickEvent { + point: { + x: number; + y: number; + }; + originalEvent: MouseEvent; // Adjust based on your event structure +} + export interface MapProps { id: string; interactive?: boolean; @@ -162,6 +175,77 @@ const MapComponent: React.FC = ({ onClick, }) => { const { map, bodyId } = useMapWithStyle({ centerGeoUri, onError, id, interactive, bounds, allowGeolocate }); + const [annotations, setAnnotations] = useState([]); // Manage annotations state + const matrixClient = useContext(MatrixClientContext); + const roomId = SdkContextClass.instance.roomViewStore.getRoomId(); + + useEffect(() => { + console.log("Updated annotations:", annotations); + }, [annotations]); + + useEffect(() => { + console.log("Current roomId:", roomId); + const fetchAnnotations = async () => { + if (!roomId || !matrixClient) { + return; + } + const annotations = await loadAnnotations(roomId, matrixClient); + setAnnotations(annotations); + }; + fetchAnnotations(); + }, [roomId, matrixClient]); + + const handleSaveAnnotation = (annotation: Annotation) => { + setAnnotations(prevAnnotations => { + const updatedAnnotations = [...prevAnnotations, annotation]; + + if (!roomId || !matrixClient) { + return prevAnnotations; + } + // Send the updated annotations to the server without awaiting + saveAnnotation(roomId, matrixClient, updatedAnnotations); + console.log("Annotation saved successfully!"); + + return updatedAnnotations; // Return the updated state + }); + }; + + const handleDeleteAnnotation = (geoUri: string) => { + setAnnotations(prevAnnotations => { + // Create a new array with the annotation removed + const updatedAnnotations = prevAnnotations.filter(annotation => annotation.geoUri !== geoUri); + + if (!roomId || !matrixClient) { + return prevAnnotations; + } + // Send the updated annotations to the server without awaiting + deleteAnnotation(roomId, matrixClient, geoUri, updatedAnnotations); + console.log("Annotation deleted successfully!"); + + return updatedAnnotations; // Return the updated state + }); + }; + + useEffect(() => { + if (map) { + const handleMapClick = (event: any) => { + const isAltClick = event.originalEvent.altKey; // Check if Alt key is pressed + + if (isAltClick) { + openDialog(event); + } + + onClick?.(); + }; + + map.on('click', handleMapClick); + + // Cleanup the event listener on component unmount + return () => { + map.off('click', handleMapClick); + }; + } + }, [map, onClick]); const onMapClick = (event: React.MouseEvent): void => { // Eat click events when clicking the attribution button @@ -173,9 +257,50 @@ const MapComponent: React.FC = ({ onClick?.(); }; + const openDialog = (event: ClickEvent) => { + Modal.createDialog>(AnnotationDialog, { + onSubmit: (title: string, color: string) => { + handleDialogSubmit(title, color, event); + }, + onFinished: () => { + // Handle dialog close if necessary + }, + }); + }; + + const handleDialogSubmit = (title: string, color: string, clickEvent: ClickEvent) => { + if (clickEvent && map) { + const { lng, lat } = map.unproject([clickEvent.point.x, clickEvent.point.y]); + + const newAnnotation = { + geoUri: `geo:${lat},${lng}`, // Format Geo URI + body: title, + color: color, // Include color in the annotation + }; + + handleSaveAnnotation?.(newAnnotation); + } + }; + + const onDelete = (key: string) => { + handleDeleteAnnotation?.(key); + }; + return (
{!!children && !!map && children({ map })} + + {map && annotations && annotations.map((annotation) => ( + + ))}
); }; From fb20d994e60f2dca91ad57760c2c23278047992b Mon Sep 17 00:00:00 2001 From: toshanmugaraj Date: Mon, 10 Feb 2025 09:38:52 +0300 Subject: [PATCH 2/3] updated the package for matrix-js-sdk --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8558639470f..304f2c21893 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "github:toshanmugaraj/matrix-js-sdk#mapannotation", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", @@ -307,4 +307,4 @@ "engines": { "node": ">=20.0.0" } -} +} \ No newline at end of file From 10d337223aaaa6b7935d192a21bba72c5c975516 Mon Sep 17 00:00:00 2001 From: toshanmugaraj Date: Thu, 20 Feb 2025 09:12:22 +0300 Subject: [PATCH 3/3] Using custom events for map annotations --- package.json | 2 +- .../views/location/AnnotationMarker.tsx | 7 - .../views/location/AnnotationPin.ts | 125 +++++++----------- 3 files changed, 49 insertions(+), 85 deletions(-) diff --git a/package.json b/package.json index 304f2c21893..85a2bbee457 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "maplibre-gl": "^5.0.0", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "0.0.1", - "matrix-js-sdk": "github:toshanmugaraj/matrix-js-sdk#mapannotation", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^1.10.0", "memoize-one": "^6.0.0", "mime": "^4.0.4", diff --git a/src/components/views/location/AnnotationMarker.tsx b/src/components/views/location/AnnotationMarker.tsx index 7171bf70ca2..f31e5c2b937 100644 --- a/src/components/views/location/AnnotationMarker.tsx +++ b/src/components/views/location/AnnotationMarker.tsx @@ -2,13 +2,6 @@ import React, { ReactNode, useState } from "react"; import classNames from "classnames"; import LocationIcon from "@vector-im/compound-design-tokens/assets/web/icons/location-pin-solid"; - - - -/** - * Wrap with tooltip handlers when - * tooltip is truthy - */ const OptionalTooltip: React.FC<{ tooltip?: React.ReactNode; annotationKey: string; diff --git a/src/components/views/location/AnnotationPin.ts b/src/components/views/location/AnnotationPin.ts index 803e02ae48f..15c7c861139 100644 --- a/src/components/views/location/AnnotationPin.ts +++ b/src/components/views/location/AnnotationPin.ts @@ -1,23 +1,24 @@ -import { EventTimeline, EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, Room, StateEvents, TimelineEvents } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import Annotation from "./Annotation"; -import { RoomAnnotationEventContent } from "matrix-js-sdk/src/@types/state_events"; -import * as MegolmExportEncryptionExport from "../../../../src/utils/MegolmExportEncryption" +enum CustomEventType { + MapAnnotation = "m.map.annotation" +} -export const fetchPinnedEvent = async (roomId: string, matrixClient: MatrixClient): Promise => { +export const fetchAnnotationEvent = async (roomId: string, matrixClient: MatrixClient): Promise => { try { const room = matrixClient.getRoom(roomId); if (!room) { console.error("Room not found"); return null; } - const readPinsEventId = getPinnedEventIds(room)[0]; - if (!readPinsEventId) { + const annotationEventId = getAnnotationEventId(room); + if (!annotationEventId) { console.error("Read pins event ID not found"); return null; } - let localEvent = room.findEventById(readPinsEventId); + let localEvent = room.findEventById(annotationEventId); // Decrypt if necessary if (localEvent?.isEncrypted()) { @@ -35,20 +36,19 @@ export const fetchPinnedEvent = async (roomId: string, matrixClient: MatrixClien return null; }; -function getPinnedEventIds(room?: Room): string[] { - const eventIds: string[] = - room - ?.getLiveTimeline() - .getState(EventTimeline.FORWARDS) - ?.getStateEvents(EventType.RoomPinnedEvents, "") - ?.getContent()?.pinned ?? []; - // Limit the number of pinned events to 100 - return eventIds.slice(0, 1); +function getAnnotationEventId(room?: Room): string { + const events = room?.currentState.getStateEvents(CustomEventType.MapAnnotation); + if (!events || events.length === 0) { + return ""; // Return an empty array if no events are found + } + const content = events[0].getContent(); // Get content from the event + const annotationEventId = content.event_id || ""; + return annotationEventId; } -export async function extractAnnotationsPassphrase(roomId: string, matrixClient: MatrixClient): Promise { +export async function extractAnnotations(roomId: string, matrixClient: MatrixClient): Promise { try { - const pinEvent = await fetchPinnedEvent(roomId, matrixClient); + const pinEvent = await fetchAnnotationEvent(roomId, matrixClient); if(!pinEvent) { return null; } @@ -60,14 +60,15 @@ export async function extractAnnotationsPassphrase(roomId: string, matrixClient: console.error("Error retrieving content from pinned event:", error); return null; } - return null; } // Function to update annotations on the server -export const sendAnnotations = async (roomId: string, content: RoomAnnotationEventContent, matrixClient: MatrixClient) => { +export const sendAnnotations = async (roomId: string, content: TimelineEvents[keyof TimelineEvents], matrixClient: MatrixClient) => { try { - await matrixClient.sendStateEvent(roomId, EventType.RoomAnnotation, content); + + let eventid = await matrixClient.sendEvent(roomId, (CustomEventType.MapAnnotation as unknown) as keyof TimelineEvents, content); + await matrixClient.sendStateEvent(roomId, (CustomEventType.MapAnnotation as unknown) as keyof StateEvents, eventid); console.log("Annotations updated successfully!"); } catch (error) { console.error("Failed to update annotations:", error); @@ -80,14 +81,14 @@ export const sendAnnotations = async (roomId: string, content: RoomAnnotationEve // Function to save an annotation export const saveAnnotation = async (roomId: string, matrixClient: MatrixClient, annotations: Annotation[]) => { try { - - const base64EncryptedAnnotations = await encryptAnnotations(roomId, matrixClient, annotations); - if(!base64EncryptedAnnotations) { + // Convert annotations to a string + const stringifiedAnnotations = JSON.stringify(annotations); + if (!stringifiedAnnotations) { return []; } const content = { - annotations: base64EncryptedAnnotations, // Use the base64 string - }; + annotations: stringifiedAnnotations, // Use the stringified annotations + } as unknown as TimelineEvents[keyof TimelineEvents]; await sendAnnotations(roomId, content, matrixClient); } catch (error) { @@ -100,19 +101,18 @@ export const saveAnnotation = async (roomId: string, matrixClient: MatrixClient, // Function to delete an annotation export const deleteAnnotation = async (roomId: string, matrixClient: MatrixClient, geoUri: string, annotations: Annotation[]) => { try { - // Prepare content for the server - const base64EncryptedAnnotations = await encryptAnnotations(roomId, matrixClient, annotations); - if(!base64EncryptedAnnotations) { + // Convert annotations to a string + const stringifiedAnnotations = JSON.stringify(annotations); + if (!stringifiedAnnotations) { return []; } const content = { - annotations: base64EncryptedAnnotations, // Use the base64 string - }; + annotations: stringifiedAnnotations, // Use the stringified annotations + } as unknown as TimelineEvents[keyof TimelineEvents]; await sendAnnotations(roomId, content, matrixClient); } catch (error) { console.error("Failed to delete annotation:", error); - // Handle the error appropriately (e.g., notify the user) throw error; // Optionally rethrow the error for further handling } }; @@ -121,27 +121,32 @@ export const deleteAnnotation = async (roomId: string, matrixClient: MatrixClien export const loadAnnotations = async (roomId: string, matrixClient: MatrixClient): Promise => { try { const room = matrixClient.getRoom(roomId); // Get the room object - const events = room?.currentState.getStateEvents(EventType.RoomAnnotation); + const events = room?.currentState.getStateEvents("m.map.annotation"); if (!events || events.length === 0) { return []; // Return an empty array if no events are found } - const password = await extractAnnotationsPassphrase(roomId, matrixClient); - if(!password) { + + const event = await fetchAnnotationEvent(roomId, matrixClient); + if (!event) { return []; } - const event = events[0]; + const content = event.getContent(); // Get content from the event - const encryptedAnnotations = content.annotations || []; - if (typeof encryptedAnnotations !== 'string') { + const stringifiedAnnotations = content.annotations || []; + if (typeof stringifiedAnnotations !== 'string') { console.warn("Content is not a string. Returning early."); return []; // or handle accordingly } - const decryptedArray: Annotation[] = await decryptAnnotations(encryptedAnnotations, password); - if (Object.keys(content.annotations).length === 0) { - return []; + + // Parse the JSON string back to an array of annotations + const annotationsArray: Annotation[] = JSON.parse(stringifiedAnnotations); + + if (!Array.isArray(annotationsArray) || annotationsArray.length === 0) { + return []; // Return an empty array if parsing fails or array is empty } - return decryptedArray.map((annotation: { geoUri: string; body: string; color?: string; }) => ({ + + return annotationsArray.map((annotation: { geoUri: string; body: string; color?: string; }) => ({ geoUri: annotation.geoUri, body: annotation.body, color: annotation.color, @@ -151,38 +156,4 @@ export const loadAnnotations = async (roomId: string, matrixClient: MatrixClient console.error("Failed to load annotations:", error); return []; // Return an empty array in case of an error } -}; - -export const decryptAnnotations = async (encryptedAnnotations: string, password: string): Promise => { - try { - // Convert from base64 string to ArrayBuffer - const binaryString = atob(encryptedAnnotations); - const charCodeArray = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - charCodeArray[i] = binaryString.charCodeAt(i); - } - const arrayBuffer = charCodeArray.buffer; - - // Decrypt the annotations - const decryptedAnnotations = await MegolmExportEncryptionExport.decryptMegolmKeyFile(arrayBuffer, password); - return JSON.parse(decryptedAnnotations); // Convert decrypted string back to JSON - } catch (error) { - console.error("Decryption failed:", error); - throw error; // Rethrow the error for further handling - } -}; - -export const encryptAnnotations = async (roomId: string, matrixClient: MatrixClient, annotations: Annotation[]): Promise => { - const jsonAnnotations = JSON.stringify(annotations); - const password = await extractAnnotationsPassphrase(roomId, matrixClient); - - if (!password) { - return null; - } - - const encryptedAnnotations = await MegolmExportEncryptionExport.encryptMegolmKeyFile(jsonAnnotations, password, { kdf_rounds: 1000 }); - const encryptedAnnotationsArray = new Uint8Array(encryptedAnnotations); - const base64EncryptedAnnotations = btoa(String.fromCharCode(...encryptedAnnotationsArray)); - - return base64EncryptedAnnotations; // Return the base64 encoded encrypted annotations -} \ No newline at end of file +}; \ No newline at end of file