From 36ae6fdfd052d617c77db69341a25d8ec4ea6094 Mon Sep 17 00:00:00 2001 From: Salastil Date: Thu, 10 Sep 2026 21:20:16 -0400 Subject: [PATCH] Somewhere to send a location from `sendMatrixLocation` has worked since #123 and had no caller anywhere in the app. That issue was "stickers and locations can be read but not sent" and was closed with the sticker half wired up and this half reachable by nothing. A field rather than a map, deliberately. Drawing one means fetching tiles from somebody's tile server, and the receiving side already decided against that - a message that mentions a street corner should not tell a third party it was read. Sending should not quietly undo the decision receiving made. So it is a box, and the daemon does the hard part: it already accepts a pasted map link, a `geo:` URI, or two numbers with a comma between them. That is what people have to hand - nobody types coordinates from memory, they copy a link. The label is offered first and the coordinates second, because the label is the part a person reads: a pair of numbers arriving with no name is a pin in the middle of nowhere as far as the recipient is concerned. Its own button beside the sticker one, for the reason the sticker button sits beside the emoji one: a location is the message rather than something typed into it. Matrix only, which is the one service here that carries a place as its own kind of message. Closes #166. Co-Authored-By: Claude Opus 5 --- nobilis | 2 +- src/renderer/src/app.css | 20 +++++ src/renderer/src/components/Composer.tsx | 37 +++++++++ src/renderer/src/components/PlaceField.tsx | 90 ++++++++++++++++++++++ 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/PlaceField.tsx diff --git a/nobilis b/nobilis index 61bcc34..b4055cb 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit 61bcc34ae949c05d7dd45d6f639f1a712ae40697 +Subproject commit b4055cb915894222a52bc204adc6b0568da23051 diff --git a/src/renderer/src/app.css b/src/renderer/src/app.css index 0571941..0a86941 100644 --- a/src/renderer/src/app.css +++ b/src/renderer/src/app.css @@ -4522,6 +4522,26 @@ video.lightbox-media { padding-top: var(--spacing-xs); } +/* Sending a place: two fields and a button. Deliberately not a map - drawing + one means fetching tiles from somebody's server, and the receive side + already decided a message mentioning a street corner should not tell a + third party it was read. */ +.place-form { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.place-form code { + font-family: var(--font-mono); + font-size: 0.9em; +} + +.place-actions { + display: flex; + justify-content: flex-end; +} + /* Making an invite: three choices and the link they produce. A column because each row is a question and its answer, and a form read down the page is a form somebody finishes. */ diff --git a/src/renderer/src/components/Composer.tsx b/src/renderer/src/components/Composer.tsx index b4acdfc..d694bf1 100644 --- a/src/renderer/src/components/Composer.tsx +++ b/src/renderer/src/components/Composer.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { Icon, IconButton } from './Icon' import { Avatar } from './Avatar' import { EmojiPicker, type StickerEntry } from './EmojiPicker' +import { PlaceField } from './PlaceField' import { useActiveBuffer, useChat, useStore } from '../state/hooks' import { emojiPreview } from '../lib/format' import { bufferDisplayName, classes, fileNameOf, isImageFile, resolveMediaUrl } from '../lib/util' @@ -100,6 +101,8 @@ export function Composer(): JSX.Element | null { /** The account's sticker packs, fetched when the picker is first opened. */ const [stickers, setStickers] = useState([]) const [stickerPicker, setStickerPicker] = useState(false) + /** The little form for sending a place, which is a field rather than a map. */ + const [placeOpen, setPlaceOpen] = useState(false) const seqRef = useRef(0) const typingSentAt = useRef(0) /** @@ -154,6 +157,7 @@ export function Composer(): JSX.Element | null { const [roles, setRoles] = useState<{ id: string; name: string; colour?: string }[]>([]) const emojiButtonRef = useRef(null) const stickerButtonRef = useRef(null) + const placeButtonRef = useRef(null) const smilies = useChat((s) => s.smilies) const bufferEmojiByBuffer = useChat((s) => s.bufferEmoji) @@ -798,6 +802,26 @@ export function Composer(): JSX.Element | null { )} + {/* Beside the sticker button for the same reason it is beside the + emoji one: a location is the message rather than something typed + into it. Matrix only, which is the one service here that carries + a place as its own kind of message. */} + {service === 'matrix' && ( + + )} + + {placeOpen && ( + setPlaceOpen(false)} + onSend={(place, label) => { + setPlaceOpen(false) + void window.moho + .rpc('sendMatrixLocation', { bufferId: buffer.id, place, label }) + .catch((e: Error) => store.toast('error', e.message)) + }} + /> + )} + {stickerPicker && ( void + onClose: () => void +}): JSX.Element { + const [place, setPlace] = useState('') + const [label, setLabel] = useState('') + const field = useRef(null) + + useEffect(() => { + field.current?.focus() + }, []) + + const send = (): void => { + const trimmed = place.trim() + if (!trimmed) return + onSend(trimmed, label.trim()) + } + + return ( + +
+
+ Paste a map link, or type coordinates as 51.5, -0.12. +
+ +
+ + setPlace(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') send() + if (e.key === 'Escape') onClose() + }} + /> +
+ +
+ + setLabel(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') send() + if (e.key === 'Escape') onClose() + }} + /> +
+ +
+ +
+
+
+ ) +}