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() + }} + /> +
+ +
+ +
+
+
+ ) +}