forked from Mirrors/gomuks
web/timeline: add support for location messages
This commit is contained in:
parent
b8fe8372f2
commit
68385fef5d
10 changed files with 201 additions and 3 deletions
25
web/package-lock.json
generated
25
web/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<MapProvider>({
|
||||
displayName: "Map provider",
|
||||
description: "The map provider to use for location messages.",
|
||||
allowedValues: mapProviders,
|
||||
allowedContexts: anyContext,
|
||||
defaultValue: "leaflet",
|
||||
}),
|
||||
leaflet_tile_template: new Preference<string>({
|
||||
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<ContentURI>({
|
||||
displayName: "Custom notification sound",
|
||||
description: "The mxc:// URI to a custom notification sound.",
|
||||
|
|
49
web/src/ui/timeline/content/Leaflet.tsx
Normal file
49
web/src/ui/timeline/content/Leaflet.tsx
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
import L from "leaflet"
|
||||
import "leaflet/dist/leaflet.css"
|
||||
import { HTMLAttributes, useLayoutEffect, useRef } from "react"
|
||||
|
||||
const attribution = `© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors`
|
||||
|
||||
export interface GomuksLeafletProps extends HTMLAttributes<HTMLDivElement> {
|
||||
tileTemplate: string
|
||||
lat: number
|
||||
long: number
|
||||
prec: number
|
||||
marker: string
|
||||
}
|
||||
|
||||
const GomuksLeaflet = ({ tileTemplate, lat, long, prec, marker, ...rest }: GomuksLeafletProps) => {
|
||||
const ref = useRef<HTMLDivElement>(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 <div {...rest} ref={ref}/>
|
||||
}
|
||||
|
||||
export default GomuksLeaflet
|
73
web/src/ui/timeline/content/LocationMessageBody.tsx
Normal file
73
web/src/ui/timeline/content/LocationMessageBody.tsx
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
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 = <div className="location-importer"><GridLoader color="var(--primary-color)" size={25}/></div>
|
||||
|
||||
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 <div className="location-container leaflet">
|
||||
<Suspense fallback={locationLoader}>
|
||||
<GomuksLeaflet tileTemplate={tileTemplate} lat={lat} long={long} prec={prec} marker={marker}/>
|
||||
</Suspense>
|
||||
</div>
|
||||
} else if (mapProvider === "google") {
|
||||
const url = `https://www.google.com/maps/embed/v1/place?key=${GOOGLE_MAPS_API_KEY}&q=${lat},${long}`
|
||||
return <iframe className="location-container google" loading="lazy" referrerPolicy="no-referrer" src={url} />
|
||||
} else {
|
||||
return <div className="location-container blank">
|
||||
<a
|
||||
href={`https://www.openstreetmap.org/?mlat=${lat}&mlon=${long}`}
|
||||
target="_blank" rel="noreferrer noopener"
|
||||
>
|
||||
{marker}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default LocationMessageBody
|
|
@ -238,3 +238,26 @@ div.media-container {
|
|||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
iframe.location-container.google {
|
||||
height: 25rem;
|
||||
width: 100%;
|
||||
max-width: 50rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
div.location-container.leaflet {
|
||||
height: 25rem;
|
||||
max-width: 50rem;
|
||||
|
||||
> div {
|
||||
height: 25rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> div.location-importer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { MemDBEvent } from "@/api/types"
|
|||
import ACLBody from "./ACLBody.tsx"
|
||||
import EncryptedBody from "./EncryptedBody.tsx"
|
||||
import HiddenEvent from "./HiddenEvent.tsx"
|
||||
import LocationMessageBody from "./LocationMessageBody.tsx"
|
||||
import MediaMessageBody from "./MediaMessageBody.tsx"
|
||||
import MemberBody from "./MemberBody.tsx"
|
||||
import PinnedEventsBody from "./PinnedEventsBody.tsx"
|
||||
|
@ -20,6 +21,7 @@ export { default as ContentErrorBoundary } from "./ContentErrorBoundary.tsx"
|
|||
export { default as EncryptedBody } from "./EncryptedBody.tsx"
|
||||
export { default as HiddenEvent } from "./HiddenEvent.tsx"
|
||||
export { default as MediaMessageBody } from "./MediaMessageBody.tsx"
|
||||
export { default as LocationMessageBody } from "./LocationMessageBody.tsx"
|
||||
export { default as MemberBody } from "./MemberBody.tsx"
|
||||
export { default as PinnedEventsBody } from "./PinnedEventsBody.tsx"
|
||||
export { default as PowerLevelBody } from "./PowerLevelBody.tsx"
|
||||
|
@ -53,8 +55,7 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo
|
|||
}
|
||||
return MediaMessageBody
|
||||
case "m.location":
|
||||
// return LocationMessageBody
|
||||
// fallthrough
|
||||
return LocationMessageBody
|
||||
default:
|
||||
return UnknownMessageBody
|
||||
}
|
||||
|
|
3
web/src/util/keys.ts
Normal file
3
web/src/util/keys.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const TENOR_API_KEY = "AIzaSyDCLXZasjWAGuxEGggz7ND-2BFHNIDyhGw"
|
||||
export const GOOGLE_MAPS_API_KEY = "AIzaSyDOrgHKWCoWyceDPAZDNQ0zU0vWwH8BzM4"
|
||||
export const GIPHY_API_KEY = "HQku8974Uq5MZn3MZns46kXn2R4GDm75"
|
|
@ -11,7 +11,7 @@ export default defineConfig({
|
|||
manualChunks: id => {
|
||||
if (id.includes("wailsio")) {
|
||||
return "wails"
|
||||
} else if (id.includes("node_modules") && !id.includes("katex")) {
|
||||
} else if (id.includes("node_modules") && !id.includes("katex") && !id.includes("leaflet")) {
|
||||
return "vendor"
|
||||
} else if (id.endsWith("/emoji/data.json")) {
|
||||
return "emoji"
|
||||
|
|
Loading…
Add table
Reference in a new issue