diff --git a/pkg/hicli/json-commands.go b/pkg/hicli/json-commands.go index e05d256..ce33f18 100644 --- a/pkg/hicli/json-commands.go +++ b/pkg/hicli/json-commands.go @@ -42,7 +42,7 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any }) case "send_message": return unmarshalAndCall(req.Data, func(params *sendMessageParams) (*database.Event, error) { - return h.SendMessage(ctx, params.RoomID, params.BaseContent, params.Text, params.RelatesTo, params.Mentions) + return h.SendMessage(ctx, params.RoomID, params.BaseContent, params.Extra, params.Text, params.RelatesTo, params.Mentions) }) case "send_event": return unmarshalAndCall(req.Data, func(params *sendEventParams) (*database.Event, error) { @@ -173,6 +173,7 @@ type cancelRequestParams struct { type sendMessageParams struct { RoomID id.RoomID `json:"room_id"` BaseContent *event.MessageEventContent `json:"base_content"` + Extra map[string]any `json:"extra"` Text string `json:"text"` RelatesTo *event.RelatesTo `json:"relates_to"` Mentions *event.Mentions `json:"mentions"` diff --git a/pkg/hicli/send.go b/pkg/hicli/send.go index 1f403dc..2c989d9 100644 --- a/pkg/hicli/send.go +++ b/pkg/hicli/send.go @@ -66,6 +66,7 @@ func (h *HiClient) SendMessage( ctx context.Context, roomID id.RoomID, base *event.MessageEventContent, + extra map[string]any, text string, relatesTo *event.RelatesTo, mentions *event.Mentions, @@ -147,7 +148,7 @@ func (h *HiClient) SendMessage( content.RelatesTo = relatesTo } } - return h.send(ctx, roomID, event.EventMessage, &content, origText) + return h.send(ctx, roomID, event.EventMessage, &event.Content{Parsed: content, Raw: extra}, origText) } func (h *HiClient) MarkRead(ctx context.Context, roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType) error { diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index 38277c9..a84bbc3 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -53,6 +53,7 @@ export class ErrorResponse extends Error { export interface SendMessageParams { room_id: RoomID base_content?: MessageEventContent + extra?: Record text: string media_path?: string relates_to?: RelatesTo diff --git a/web/src/icons/location.svg b/web/src/icons/location.svg new file mode 100644 index 0000000..78108fd --- /dev/null +++ b/web/src/icons/location.svg @@ -0,0 +1 @@ + diff --git a/web/src/ui/composer/MessageComposer.css b/web/src/ui/composer/MessageComposer.css index ae436e3..6c02fd1 100644 --- a/web/src/ui/composer/MessageComposer.css +++ b/web/src/ui/composer/MessageComposer.css @@ -36,7 +36,7 @@ div.message-composer { } } - > div.composer-media { + > div.composer-media, > div.composer-location { display: flex; padding: .5rem; justify-content: space-between; @@ -47,4 +47,19 @@ div.message-composer { padding: .5rem; } } + + > div.composer-location { + height: 15rem; + + > div.location-container { + height: 15rem; + max-width: 40rem; + width: 100%; + + > div { + height: 15rem; + width: 100%; + } + } + } } diff --git a/web/src/ui/composer/MessageComposer.tsx b/web/src/ui/composer/MessageComposer.tsx index 5371e89..9d766d0 100644 --- a/web/src/ui/composer/MessageComposer.tsx +++ b/web/src/ui/composer/MessageComposer.tsx @@ -15,7 +15,8 @@ // along with this program. If not, see . import React, { use, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react" import { ScaleLoader } from "react-spinners" -import { useRoomEvent } from "@/api/statestore" +import Client from "@/api/client.ts" +import { RoomStateStore, usePreference, useRoomEvent } from "@/api/statestore" import type { EventID, MediaMessageEventContent, @@ -31,6 +32,7 @@ import useEvent from "@/util/useEvent.ts" import ClientContext from "../ClientContext.ts" import EmojiPicker from "../emojipicker/EmojiPicker.tsx" import { keyToString } from "../keybindings.ts" +import { LeafletPicker } from "../maps/async.tsx" import { ModalContext } from "../modal/Modal.tsx" import { useRoomContext } from "../roomview/roomcontext.ts" import { ReplyBody } from "../timeline/ReplyBody.tsx" @@ -40,12 +42,20 @@ import { charToAutocompleteType, emojiQueryRegex, getAutocompleter } from "./get import AttachIcon from "@/icons/attach.svg?react" import CloseIcon from "@/icons/close.svg?react" import EmojiIcon from "@/icons/emoji-categories/smileys-emotion.svg?react" +import LocationIcon from "@/icons/location.svg?react" import SendIcon from "@/icons/send.svg?react" import "./MessageComposer.css" +export interface ComposerLocationValue { + lat: number + long: number + prec?: number +} + export interface ComposerState { text: string media: MediaMessageEventContent | null + location: ComposerLocationValue | null replyTo: EventID | null uninited?: boolean } @@ -53,7 +63,7 @@ export interface ComposerState { const isMobileDevice = window.ontouchstart !== undefined && window.innerWidth < 800 const MAX_TEXTAREA_ROWS = 10 -const emptyComposer: ComposerState = { text: "", media: null, replyTo: null } +const emptyComposer: ComposerState = { text: "", media: null, replyTo: null, location: null } const uninitedComposer: ComposerState = { ...emptyComposer, uninited: true } const composerReducer = (state: ComposerState, action: Partial) => ({ ...state, ...action, uninited: undefined }) @@ -121,7 +131,7 @@ const MessageComposer = () => { }, [room.roomID]) const sendMessage = useEvent((evt: React.FormEvent) => { evt.preventDefault() - if (state.text === "" && !state.media) { + if (state.text === "" && !state.media && !state.location) { return } if (editing) { @@ -156,9 +166,30 @@ const MessageComposer = () => { relates_to.is_falling_back = false } } + let base_content: MessageEventContent | undefined + let extra: Record | undefined + if (state.media) { + base_content = state.media + } else if (state.location) { + base_content = { + body: "Location", + msgtype: "m.location", + geo_uri: `geo:${state.location.lat},${state.location.long}`, + } + extra = { + "org.matrix.msc3488.asset": { + type: "m.pin", + }, + "org.matrix.msc3488.location": { + uri: `geo:${state.location.lat},${state.location.long}`, + description: state.text, + }, + } + } client.sendMessage({ room_id: room.roomID, - base_content: state.media ?? undefined, + base_content, + extra, text: state.text, relates_to, mentions, @@ -290,7 +321,7 @@ const MessageComposer = () => { if (!res.ok) { throw new Error(json.error) } else { - setState({ media: json }) + setState({ media: json, location: null }) } }) .catch(err => window.alert("Failed to upload file: " + err)) @@ -352,14 +383,15 @@ const MessageComposer = () => { if (state.uninited || editing) { return } - if (!state.text && !state.media && !state.replyTo) { + if (!state.text && !state.media && !state.replyTo && !state.location) { draftStore.clear(room.roomID) } else { draftStore.set(room.roomID, state) } }, [roomCtx, room, state, editing]) const openFilePicker = useCallback(() => fileInput.current!.click(), []) - const clearMedia = useCallback(() => setState({ media: null }), []) + const clearMedia = useCallback(() => setState({ media: null, location: null }), []) + const onChangeLocation = useCallback((location: ComposerLocationValue) => setState({ location }), []) const closeReply = useCallback((evt: React.MouseEvent) => { evt.stopPropagation() setState({ replyTo: null }) @@ -385,7 +417,22 @@ const MessageComposer = () => { onClose: () => textInput.current?.focus(), }) }) + const openLocationPicker = useEvent(() => { + setState({ location: { lat: 0, long: 0, prec: 1 }, media: null }) + }) const Autocompleter = getAutocompleter(autocomplete, client, room) + let mediaDisabledTitle: string | undefined + let locationDisabledTitle: string | undefined + if (state.media) { + mediaDisabledTitle = "You can only attach one file at a time" + locationDisabledTitle = "You can't attach a location to a message with a file" + } else if (state.location) { + mediaDisabledTitle = "You can't attach a file to a message with a location" + locationDisabledTitle = "You can only attach one location at a time" + } else if (loadingMedia) { + mediaDisabledTitle = "Uploading file..." + locationDisabledTitle = "You can't attach a location to a message with a file" + } return <> {Autocompleter && autocomplete &&
{ />} {loadingMedia &&
} {state.media && } + {state.location && }