Skip to content

Commit

Permalink
Merge pull request #1 from toshanmugaraj/eventAnnotation
Browse files Browse the repository at this point in the history
Using custom events for map annotations
  • Loading branch information
toshanmugaraj authored Feb 23, 2025
2 parents fb20d99 + 10d3372 commit 9b0055d
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 85 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 0 additions & 7 deletions src/components/views/location/AnnotationMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
125 changes: 48 additions & 77 deletions src/components/views/location/AnnotationPin.ts
Original file line number Diff line number Diff line change
@@ -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<MatrixEvent | null> => {
export const fetchAnnotationEvent = async (roomId: string, matrixClient: MatrixClient): Promise<MatrixEvent | null> => {
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()) {
Expand All @@ -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<string | null> {
export async function extractAnnotations(roomId: string, matrixClient: MatrixClient): Promise<string | null> {
try {
const pinEvent = await fetchPinnedEvent(roomId, matrixClient);
const pinEvent = await fetchAnnotationEvent(roomId, matrixClient);
if(!pinEvent) {
return null;
}
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -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
}
};
Expand All @@ -121,27 +121,32 @@ export const deleteAnnotation = async (roomId: string, matrixClient: MatrixClien
export const loadAnnotations = async (roomId: string, matrixClient: MatrixClient): Promise<Annotation[]> => {
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,
Expand All @@ -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<Annotation[]> => {
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<string | null> => {
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
}
};

0 comments on commit 9b0055d

Please sign in to comment.