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 <[email protected]>
This commit is contained in:
+1
-1
Submodule nobilis updated: 61bcc34ae9...b4055cb915
@@ -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. */
|
||||
|
||||
@@ -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<StickerEntry[]>([])
|
||||
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<HTMLButtonElement>(null)
|
||||
const stickerButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const placeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const smilies = useChat((s) => s.smilies)
|
||||
const bufferEmojiByBuffer = useChat((s) => s.bufferEmoji)
|
||||
@@ -798,6 +802,26 @@ export function Composer(): JSX.Element | null {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 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' && (
|
||||
<button
|
||||
ref={placeButtonRef}
|
||||
type="button"
|
||||
className="icon-button"
|
||||
title="Send a location"
|
||||
onClick={() => {
|
||||
setPickerOpen(false)
|
||||
setStickerPicker(false)
|
||||
setPlaceOpen(!placeOpen)
|
||||
}}
|
||||
>
|
||||
<Icon name="location_on" size={18} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="add"
|
||||
title={
|
||||
@@ -809,6 +833,19 @@ export function Composer(): JSX.Element | null {
|
||||
<IconButton name="send" title="Send" onClick={submit} />
|
||||
</div>
|
||||
|
||||
{placeOpen && (
|
||||
<PlaceField
|
||||
anchor={placeButtonRef.current}
|
||||
onClose={() => 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 && (
|
||||
<EmojiPicker
|
||||
anchor={stickerButtonRef.current}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Icon } from './Icon'
|
||||
import { HeaderPopover } from './HeaderPopover'
|
||||
|
||||
/**
|
||||
* Sending a place, as a field rather than as a map.
|
||||
*
|
||||
* Deliberately not a map picker. Drawing one means fetching tiles from
|
||||
* somebody's tile server, and the receive 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 accepts a pasted map
|
||||
* link (OpenStreetMap, Google Maps, anything carrying the place in a query
|
||||
* parameter), a `geo:` URI, or two numbers with a comma between them. Which is
|
||||
* what people actually have to hand - nobody types coordinates from memory,
|
||||
* they copy a link.
|
||||
*
|
||||
* The label is the part a person reads. A pair of coordinates arriving with no
|
||||
* name is a pin in the middle of nowhere as far as the recipient is concerned,
|
||||
* so it is offered first and the coordinates second.
|
||||
*/
|
||||
export function PlaceField({
|
||||
anchor,
|
||||
onSend,
|
||||
onClose
|
||||
}: {
|
||||
anchor: HTMLElement | null
|
||||
onSend: (place: string, label: string) => void
|
||||
onClose: () => void
|
||||
}): JSX.Element {
|
||||
const [place, setPlace] = useState('')
|
||||
const [label, setLabel] = useState('')
|
||||
const field = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
field.current?.focus()
|
||||
}, [])
|
||||
|
||||
const send = (): void => {
|
||||
const trimmed = place.trim()
|
||||
if (!trimmed) return
|
||||
onSend(trimmed, label.trim())
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderPopover anchor={anchor} width={320} onClose={onClose}>
|
||||
<div className="place-form">
|
||||
<div className="small muted">
|
||||
Paste a map link, or type coordinates as <code>51.5, -0.12</code>.
|
||||
</div>
|
||||
|
||||
<div className="popover-field">
|
||||
<Icon name="location_on" size={16} />
|
||||
<input
|
||||
ref={field}
|
||||
type="text"
|
||||
value={place}
|
||||
placeholder="Map link or coordinates"
|
||||
onChange={(e) => setPlace(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') send()
|
||||
if (e.key === 'Escape') onClose()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="popover-field">
|
||||
<Icon name="label" size={16} />
|
||||
<input
|
||||
type="text"
|
||||
value={label}
|
||||
placeholder="What is there (optional)"
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') send()
|
||||
if (e.key === 'Escape') onClose()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="place-actions">
|
||||
<button type="button" className="button primary" disabled={!place.trim()} onClick={send}>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</HeaderPopover>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user