diff --git a/go.mod b/go.mod index 2ce031c..dbf11ec 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( golang.org/x/crypto v0.27.0 gopkg.in/yaml.v3 v3.0.1 maunium.net/go/mauflag v1.0.0 - maunium.net/go/mautrix v0.21.1-0.20241013141433-5cccf93cdc6a + maunium.net/go/mautrix v0.21.1-0.20241013193325-8d9caf0d55f4 ) require ( diff --git a/go.sum b/go.sum index a219cd8..baaf9b0 100644 --- a/go.sum +++ b/go.sum @@ -66,5 +66,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.21.1-0.20241013141433-5cccf93cdc6a h1:faC83lFuKSS9wTrR2pa1mm0JZkUHa5NA6eA0LVQSrDs= -maunium.net/go/mautrix v0.21.1-0.20241013141433-5cccf93cdc6a/go.mod h1:yIs8uVcl3ZiTuDzAYmk/B4/z9dQqegF0rcOWV4ncgko= +maunium.net/go/mautrix v0.21.1-0.20241013193325-8d9caf0d55f4 h1:LCVNEHiOT5N2J8OTC95TEdHqffDXoxnz1awcTh7XmyI= +maunium.net/go/mautrix v0.21.1-0.20241013193325-8d9caf0d55f4/go.mod h1:yIs8uVcl3ZiTuDzAYmk/B4/z9dQqegF0rcOWV4ncgko= diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 7fa84e5..39ba557 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { CachedEventDispatcher } from "../util/eventdispatcher.ts" -import type RPCClient from "./rpc.ts" +import RPCClient, { SendMessageParams } from "./rpc.ts" import { StateStore } from "./statestore.ts" import type { ClientState, @@ -49,12 +49,12 @@ export default class Client { } } - async sendMessage(roomID: RoomID, text: string, mediaPath?: string): Promise { - const room = this.store.rooms.get(roomID) + async sendMessage(params: SendMessageParams): Promise { + const room = this.store.rooms.get(params.room_id) if (!room) { throw new Error("Room not found") } - const dbEvent = await this.rpc.sendMessage(roomID, text, mediaPath) + const dbEvent = await this.rpc.sendMessage(params) if (!room.eventsByRowID.has(dbEvent.rowid)) { room.pendingEvents.push(dbEvent.rowid) room.applyEvent(dbEvent, true) diff --git a/web/src/api/rpc.ts b/web/src/api/rpc.ts index 2cea8a1..6349421 100644 --- a/web/src/api/rpc.ts +++ b/web/src/api/rpc.ts @@ -20,6 +20,7 @@ import type { EventID, EventRowID, EventType, + Mentions, PaginationResponse, RPCCommand, RPCEvent, @@ -41,6 +42,14 @@ export class ErrorResponse extends Error { } } +export interface SendMessageParams { + room_id: RoomID + text: string + media_path?: string + reply_to?: EventID + mentions?: Mentions +} + export default abstract class RPCClient { public readonly connect: CachedEventDispatcher = new CachedEventDispatcher() public readonly event: EventDispatcher = new EventDispatcher() @@ -110,8 +119,8 @@ export default abstract class RPCClient { }, this.cancelRequest.bind(this, request_id)) } - sendMessage(room_id: RoomID, text: string, media_path?: string): Promise { - return this.request("send_message", { room_id, text, media_path }) + sendMessage(params: SendMessageParams): Promise { + return this.request("send_message", params) } sendEvent(room_id: RoomID, type: EventType, content: Record): Promise { diff --git a/web/src/api/types/mxtypes.ts b/web/src/api/types/mxtypes.ts index 2764f02..2318f7e 100644 --- a/web/src/api/types/mxtypes.ts +++ b/web/src/api/types/mxtypes.ts @@ -64,11 +64,17 @@ export interface MemberEventContent { reason?: string } +export interface Mentions { + user_ids: UserID[] + room: boolean +} + export interface BaseMessageEventContent { msgtype: string body: string formatted_body?: string format?: "org.matrix.custom.html" + "m.mentions"?: Mentions } export interface TextMessageEventContent extends BaseMessageEventContent { diff --git a/web/src/ui/MessageComposer.css b/web/src/ui/MessageComposer.css index 81d7972..d4fe1c2 100644 --- a/web/src/ui/MessageComposer.css +++ b/web/src/ui/MessageComposer.css @@ -1,18 +1,23 @@ div.message-composer { - display: flex; border-top: 1px solid #ccc; - > textarea { - flex: 1; - resize: none; - font-family: sans-serif; - height: auto; - padding: .5rem; - border: none; - outline: none; - } - > button { - padding: .5rem; - border: none; - background: none; + + > div.input-area { + display: flex; + + > textarea { + flex: 1; + resize: none; + font-family: sans-serif; + height: auto; + padding: .5rem; + border: none; + outline: none; + } + + > button { + padding: .5rem; + border: none; + background: none; + } } } diff --git a/web/src/ui/MessageComposer.tsx b/web/src/ui/MessageComposer.tsx index 3b23579..d28e615 100644 --- a/web/src/ui/MessageComposer.tsx +++ b/web/src/ui/MessageComposer.tsx @@ -13,39 +13,68 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { use, useCallback, useState } from "react" -import { RoomStateStore } from "../api/statestore.ts" +import React, { use, useCallback, useRef, useState } from "react" +import { RoomStateStore } from "@/api/statestore.ts" +import { MemDBEvent, Mentions } from "@/api/types" import { ClientContext } from "./ClientContext.ts" +import ReplyBody from "./timeline/ReplyBody.tsx" import "./MessageComposer.css" interface MessageComposerProps { room: RoomStateStore + setTextRows: (rows: number) => void + replyTo: MemDBEvent | null + closeReply: () => void } -const MessageComposer = ({ room }: MessageComposerProps) => { +const MessageComposer = ({ room, replyTo, setTextRows, closeReply }: MessageComposerProps) => { const client = use(ClientContext)! const [text, setText] = useState("") + const textRows = useRef(1) const sendMessage = useCallback((evt: React.FormEvent) => { evt.preventDefault() + if (text === "") { + return + } setText("") - client.sendMessage(room.roomID, text) + setTextRows(1) + textRows.current = 1 + closeReply() + const room_id = room.roomID + const mentions: Mentions = { + user_ids: [], + room: false, + } + if (replyTo) { + mentions.user_ids.push(replyTo.sender) + } + client.sendMessage({ room_id, text, reply_to: replyTo?.event_id, mentions }) .catch(err => window.alert("Failed to send message: " + err)) - }, [text, room, client]) + }, [setTextRows, closeReply, replyTo, text, room, client]) + const onKeyDown = useCallback((evt: React.KeyboardEvent) => { + if (evt.key === "Enter" && !evt.shiftKey) { + sendMessage(evt) + } + }, [sendMessage]) + const onChange = useCallback((evt: React.ChangeEvent) => { + setText(evt.target.value) + textRows.current = evt.target.value.split("\n").length + setTextRows(textRows.current) + }, [setTextRows]) return
-