From 68385fef5d8ba7f6a142ba37a3ddd5b695f93d66 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 2 Dec 2024 16:44:35 +0200 Subject: [PATCH] web/timeline: add support for location messages --- web/package-lock.json | 25 +++++++ web/package.json | 2 + web/src/api/types/mxtypes.ts | 7 ++ web/src/api/types/preferences/preferences.ts | 15 ++++ web/src/ui/timeline/content/Leaflet.tsx | 49 +++++++++++++ .../timeline/content/LocationMessageBody.tsx | 73 +++++++++++++++++++ web/src/ui/timeline/content/index.css | 23 ++++++ web/src/ui/timeline/content/index.ts | 5 +- web/src/util/keys.ts | 3 + web/vite.config.ts | 2 +- 10 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 web/src/ui/timeline/content/Leaflet.tsx create mode 100644 web/src/ui/timeline/content/LocationMessageBody.tsx create mode 100644 web/src/util/keys.ts diff --git a/web/package-lock.json b/web/package-lock.json index 547cf8a..8fdb978 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "@wailsio/runtime": "^3.0.0-alpha.29", "blurhash": "^2.0.5", "katex": "^0.16.11", + "leaflet": "^1.9.4", "react": "^19.0.0-rc.1", "react-blurhash": "^0.3.0", "react-dom": "^19.0.0-rc.1", @@ -23,6 +24,7 @@ "devDependencies": { "@eslint/js": "^9.11.1", "@types/katex": "^0.16.7", + "@types/leaflet": "^1.9.14", "@types/sanitize-html": "^2.13.0", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.11.1", @@ -1741,6 +1743,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1762,6 +1771,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/leaflet": { + "version": "1.9.14", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.14.tgz", + "integrity": "sha512-sx2q6MDJaajwhKeVgPSvqXd8rhNJSTA3tMidQGduZn9S6WBYxDkCpSpV5xXEmSg7Cgdk/5vJGhVF1kMYLzauBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/react": { "name": "types-react", "version": "19.0.0-rc.1", @@ -4165,6 +4184,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/web/package.json b/web/package.json index 4cc2c8d..42caceb 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "@wailsio/runtime": "^3.0.0-alpha.29", "blurhash": "^2.0.5", "katex": "^0.16.11", + "leaflet": "^1.9.4", "react": "^19.0.0-rc.1", "react-blurhash": "^0.3.0", "react-dom": "^19.0.0-rc.1", @@ -25,6 +26,7 @@ "devDependencies": { "@eslint/js": "^9.11.1", "@types/katex": "^0.16.7", + "@types/leaflet": "^1.9.14", "@types/sanitize-html": "^2.13.0", "@vitejs/plugin-react-swc": "^3.5.0", "eslint": "^9.11.1", diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index f22c47e..cd3c659 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -199,6 +199,13 @@ export interface MediaInfo { export interface LocationMessageEventContent extends BaseMessageEventContent { msgtype: "m.location" geo_uri: string + "org.matrix.msc3488.asset"?: { + type?: "m.pin" + } + "org.matrix.msc3488.location"?: { + uri: string + description?: string + } } export type MessageEventContent = TextMessageEventContent | MediaMessageEventContent | LocationMessageEventContent diff --git a/web/src/api/types/preferences/preferences.ts b/web/src/api/types/preferences/preferences.ts index 34b8e3c..5200b12 100644 --- a/web/src/api/types/preferences/preferences.ts +++ b/web/src/api/types/preferences/preferences.ts @@ -26,8 +26,10 @@ export const codeBlockStyles = [ "solarized-dark", "solarized-light", "swapoff", "tango", "tokyonight-day", "tokyonight-moon", "tokyonight-night", "tokyonight-storm", "trac", "vim", "vs", "vulcan", "witchhazel", "xcode-dark", "xcode", ] as const +export const mapProviders = ["leaflet", "google", "none"] as const export type CodeBlockStyle = typeof codeBlockStyles[number] +export type MapProvider = typeof mapProviders[number] /* eslint-disable max-len */ export const preferences = { @@ -104,6 +106,19 @@ export const preferences = { allowedContexts: anyContext, defaultValue: true, }), + map_provider: new Preference({ + displayName: "Map provider", + description: "The map provider to use for location messages.", + allowedValues: mapProviders, + allowedContexts: anyContext, + defaultValue: "leaflet", + }), + leaflet_tile_template: new Preference({ + displayName: "Leaflet tile URL template", + description: "When using Leaflet for maps, the URL template for map tile images.", + allowedContexts: anyContext, + defaultValue: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + }), custom_notification_sound: new Preference({ displayName: "Custom notification sound", description: "The mxc:// URI to a custom notification sound.", diff --git a/web/src/ui/timeline/content/Leaflet.tsx b/web/src/ui/timeline/content/Leaflet.tsx new file mode 100644 index 0000000..410af0c --- /dev/null +++ b/web/src/ui/timeline/content/Leaflet.tsx @@ -0,0 +1,49 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import L from "leaflet" +import "leaflet/dist/leaflet.css" +import { HTMLAttributes, useLayoutEffect, useRef } from "react" + +const attribution = `© OpenStreetMap contributors` + +export interface GomuksLeafletProps extends HTMLAttributes { + tileTemplate: string + lat: number + long: number + prec: number + marker: string +} + +const GomuksLeaflet = ({ tileTemplate, lat, long, prec, marker, ...rest }: GomuksLeafletProps) => { + const ref = useRef(null) + useLayoutEffect(() => { + const container = ref.current + if (!container) { + return + } + const rendered = L.map(container) + rendered.setView([lat, long], prec) + const markerElem = L.marker([lat, long]).addTo(rendered) + markerElem.bindPopup(marker).openPopup() + L.tileLayer(tileTemplate, { attribution }).addTo(rendered) + return () => { + rendered.remove() + } + }, [lat, long, prec, marker, tileTemplate]) + return
+} + +export default GomuksLeaflet diff --git a/web/src/ui/timeline/content/LocationMessageBody.tsx b/web/src/ui/timeline/content/LocationMessageBody.tsx new file mode 100644 index 0000000..3a5a2a2 --- /dev/null +++ b/web/src/ui/timeline/content/LocationMessageBody.tsx @@ -0,0 +1,73 @@ +// gomuks - A Matrix client written in Go. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import { Suspense, lazy, use } from "react" +import { GridLoader } from "react-spinners" +import { usePreference } from "@/api/statestore" +import { LocationMessageEventContent } from "@/api/types" +import { GOOGLE_MAPS_API_KEY } from "@/util/keys.ts" +import { ensureString } from "@/util/validation.ts" +import ClientContext from "../../ClientContext.ts" +import EventContentProps from "./props.ts" + +const GomuksLeaflet = lazy(() => import("./Leaflet.tsx")) + +function parseGeoURI(uri: unknown): [lat: number, long: number, prec: number] { + const geoURI = ensureString(uri) + if (!geoURI.startsWith("geo:")) { + return [0, 0, 0] + } + try { + const [coordinates/*, params*/] = geoURI.slice("geo:".length).split(";") + const [lat, long] = coordinates.split(",").map(parseFloat) + // const decodedParams = new URLSearchParams(params) + const prec = 13 // (+(decodedParams.get("u") ?? 0)) || 13 + return [lat, long, prec] + } catch { + return [0, 0, 0] + } +} + +const locationLoader =
+ +const LocationMessageBody = ({ event, room }: EventContentProps) => { + const content = event.content as LocationMessageEventContent + const client = use(ClientContext)! + const mapProvider = usePreference(client.store, room, "map_provider") + const tileTemplate = usePreference(client.store, room, "leaflet_tile_template") + const [lat, long, prec] = parseGeoURI(content.geo_uri) + const marker = ensureString(content["org.matrix.msc3488.location"]?.description ?? content.body) + if (mapProvider === "leaflet") { + return
+ + + +
+ } else if (mapProvider === "google") { + const url = `https://www.google.com/maps/embed/v1/place?key=${GOOGLE_MAPS_API_KEY}&q=${lat},${long}` + return