diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx index e8738644c..659fcf2f3 100644 --- a/src/app/components/message/MsgTypeRenderers.tsx +++ b/src/app/components/message/MsgTypeRenderers.tsx @@ -37,6 +37,7 @@ import { } from '../../../types/matrix/common'; import { FALLBACK_MIMETYPE, getBlobSafeMimeType } from '../../utils/mimeTypes'; import { parseGeoUri, scaleYDimension } from '../../utils/common'; +import { formatAccuracy } from '../../utils/geoShare'; import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment'; import { FileHeader, FileDownloadButton } from './FileHeader'; @@ -732,6 +733,8 @@ export function MLocation({ content }: MLocationProps) { const lon = parseFloat(location.longitude); if (!isFinite(lat) || !isFinite(lon)) return ; const description = typeof msc3488?.description === 'string' ? msc3488.description : undefined; + // RFC 5870 `;u=` uncertainty (meters), sent by Lotus since Gitea #340. + const accuracyLabel = formatAccuracy(Number(/;u=([\d.]+)/.exec(geoUri)?.[1])); const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.007},${ lat - 0.004 },${lon + 0.007},${lat + 0.004}&layer=mapnik&marker=${lat},${lon}`; @@ -772,6 +775,7 @@ export function MLocation({ content }: MLocationProps) { )} {`${lat.toFixed(5)}, ${lon.toFixed(5)}`} + {accuracyLabel && ` (${accuracyLabel})`} + + + + + + + + + ); +} diff --git a/src/app/utils/geoShare.test.ts b/src/app/utils/geoShare.test.ts new file mode 100644 index 000000000..1f77e10c8 --- /dev/null +++ b/src/app/utils/geoShare.test.ts @@ -0,0 +1,30 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { buildGeoUri, formatAccuracy } from './geoShare'; +import { parseGeoUri } from './common'; + +test('buildGeoUri: exact share keeps 6 decimals and adds the accuracy as u=', () => { + assert.equal(buildGeoUri(40.748817, -73.985428, 12.4), 'geo:40.748817,-73.985428;u=12'); + assert.equal(buildGeoUri(40.748817, -73.985428), 'geo:40.748817,-73.985428'); + assert.equal(buildGeoUri(40.748817, -73.985428, Number.NaN), 'geo:40.748817,-73.985428'); +}); + +test('buildGeoUri: approximate share rounds to ~1 km and never claims better than 1 km', () => { + assert.equal(buildGeoUri(40.748817, -73.985428, 12, true), 'geo:40.75,-73.99;u=1000'); + assert.equal(buildGeoUri(40.748817, -73.985428, 3500, true), 'geo:40.75,-73.99;u=3500'); +}); + +test('buildGeoUri output still parses with the renderer', () => { + const loc = parseGeoUri(buildGeoUri(51.5, -0.12, 30)); + assert.ok(loc); + assert.equal(parseFloat(loc.latitude), 51.5); + assert.equal(parseFloat(loc.longitude), -0.12); +}); + +test('formatAccuracy', () => { + assert.equal(formatAccuracy(12.4), '±12 m'); + assert.equal(formatAccuracy(3500), '±3.5 km'); + assert.equal(formatAccuracy(25000), '±25 km'); + assert.equal(formatAccuracy(undefined), undefined); + assert.equal(formatAccuracy(0), undefined); +}); diff --git a/src/app/utils/geoShare.ts b/src/app/utils/geoShare.ts new file mode 100644 index 000000000..dd22aca6f --- /dev/null +++ b/src/app/utils/geoShare.ts @@ -0,0 +1,39 @@ +/** + * [Gitea #340] Helpers for the "Share location" confirm step: the geo URI to + * send (RFC 5870, with the fix's accuracy as `u=` so receivers can draw a + * radius) and a human accuracy label, so a ±3 km Wi-Fi/IP fix isn't presented + * — or sent — as if it were exact. + */ + +/** Decimals kept for an "approximate" share: 2 → ~1.1 km of latitude. */ +export const APPROXIMATE_DECIMALS = 2; +/** Uncertainty reported for an approximate share (half a 0.01° cell, rounded up). */ +export const APPROXIMATE_UNCERTAINTY_M = 1000; + +const validAccuracy = (accuracy?: number): number | undefined => + typeof accuracy === 'number' && Number.isFinite(accuracy) && accuracy > 0 + ? Math.round(accuracy) + : undefined; + +export const buildGeoUri = ( + latitude: number, + longitude: number, + accuracy?: number, + approximate = false, +): string => { + const decimals = approximate ? APPROXIMATE_DECIMALS : 6; + const u = approximate + ? Math.max(APPROXIMATE_UNCERTAINTY_M, validAccuracy(accuracy) ?? 0) + : validAccuracy(accuracy); + const base = `geo:${latitude.toFixed(decimals)},${longitude.toFixed(decimals)}`; + return u ? `${base};u=${u}` : base; +}; + +/** "±12 m", "±3.5 km", or undefined when the browser gave no accuracy. */ +export const formatAccuracy = (accuracy?: number): string | undefined => { + const m = validAccuracy(accuracy); + if (m === undefined) return undefined; + if (m < 1000) return `±${m} m`; + const km = m / 1000; + return `±${km < 10 ? km.toFixed(1) : Math.round(km)} km`; +};