From cf22fd8501008f49db23909d75f4172e5808666f Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sun, 11 Oct 2026 22:16:05 -0400 Subject: [PATCH] fix(location): confirm before sharing, with accuracy and an approximate option (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composer's location button sent the exact position (6 decimals) from a single click once the browser permission had been granted, discarding the fix's accuracy, so a ±3.5 km Wi-Fi/IP fix was shown as exact. - ShareLocationDialog: "Share your location?" with the coordinates and ±accuracy, a "Check on OpenStreetMap" link, and Cancel / Send approximate (~1 km) / Send. Focus starts on Cancel; Esc cancels. - utils/geoShare.ts (unit-tested): RFC 5870 geo URI with `;u=` accuracy; approximate shares round to 2 decimals and never claim better than 1 km. - Location cards show the ± when the event carries `u=`. - The toolbar icon is a pin instead of the globe. Verified headless (fix 40.748817,-73.985428 ±3500 m): the click opens the dialog and sends nothing; Cancel/Esc send nothing; Send approximate → geo:40.75,-73.99;u=3500; Send → geo:40.748817,-73.985428;u=3500; the card reads "40.74882, -73.98543 (±3.5 km)". Fixes #340 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- .../components/message/MsgTypeRenderers.tsx | 4 + src/app/features/room/RoomInput.tsx | 61 ++++++--- src/app/features/room/ShareLocationDialog.tsx | 124 ++++++++++++++++++ src/app/utils/geoShare.test.ts | 30 +++++ src/app/utils/geoShare.ts | 39 ++++++ 5 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 src/app/features/room/ShareLocationDialog.tsx create mode 100644 src/app/utils/geoShare.test.ts create mode 100644 src/app/utils/geoShare.ts 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`; +};