From 0be4d972d028ebab6a328a792be93f6fedd2a735 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Sun, 10 Sep 2023 16:36:44 -0400 Subject: [PATCH] send messages and typing events --- src/components/messaging/Message.tsx | 7 +- src/components/messaging/MessageInput.tsx | 100 +++++++++++++++--- .../{FileUpload.tsx => AttachmentUpload.tsx} | 3 +- .../attachments/AttachmentUploadPreview.tsx | 3 +- src/stores/MessageQueue.ts | 7 -- src/stores/objects/QueuedMessage.ts | 15 ++- 6 files changed, 101 insertions(+), 34 deletions(-) rename src/components/messaging/attachments/{FileUpload.tsx => AttachmentUpload.tsx} (96%) diff --git a/src/components/messaging/Message.tsx b/src/components/messaging/Message.tsx index 32a3146..f758da6 100644 --- a/src/components/messaging/Message.tsx +++ b/src/components/messaging/Message.tsx @@ -4,7 +4,6 @@ import React from "react"; import Moment from "react-moment"; import styled from "styled-components"; import { ContextMenuContext } from "../../contexts/ContextMenuContext"; -import { useAppStore } from "../../stores/AppStore"; import { MessageLike } from "../../stores/objects/Message"; import { calendarStrings } from "../../utils/i18n"; import Avatar from "../Avatar"; @@ -98,8 +97,6 @@ interface Props { * Component for rendering a single message */ function Message({ message, isHeader, isSending, isFailed }: Props) { - const app = useAppStore(); - const contextMenu = React.useContext(ContextMenuContext); const [contextMenuItems, setContextMenuItems] = React.useState([ { @@ -237,9 +234,7 @@ function Message({ message, isHeader, isSending, isFailed }: Props) { {renderMessageContent()} {"files" in message && message.files?.length !== 0 && ( -
- -
+ )} diff --git a/src/components/messaging/MessageInput.tsx b/src/components/messaging/MessageInput.tsx index 6605609..2109d62 100644 --- a/src/components/messaging/MessageInput.tsx +++ b/src/components/messaging/MessageInput.tsx @@ -1,15 +1,23 @@ import Channel from "../../stores/objects/Channel"; -import { ChannelType } from "@spacebarchat/spacebar-api-types/v9"; +import { + ChannelType, + MessageType, + RESTPostAPIChannelMessageJSONBody, + Routes, +} from "@spacebarchat/spacebar-api-types/v9"; import { observer } from "mobx-react-lite"; import React from "react"; import styled from "styled-components"; import useLogger from "../../hooks/useLogger"; +import { useAppStore } from "../../stores/AppStore"; import Guild from "../../stores/objects/Guild"; +import Snowflake from "../../utils/Snowflake"; import { debounce } from "../../utils/debounce"; +import { isTouchscreenDevice } from "../../utils/isTouchscreenDevice"; import MessageTextArea from "./MessageTextArea"; +import FileUpload from "./attachments/AttachmentUpload"; import AttachmentUploadList from "./attachments/AttachmentUploadPreview"; -import FileUpload from "./attachments/FileUpload"; const Container = styled.div` padding: 0 16px; @@ -62,6 +70,7 @@ interface Props { * Component for sending messages */ function MessageInput({ channel }: Props) { + const app = useAppStore(); const logger = useLogger("MessageInput"); const [content, setContent] = React.useState(""); const [uploadState, setUploadState] = React.useState({ @@ -73,11 +82,11 @@ function MessageInput({ channel }: Props) { /** * Starts typing for client user and triggers gateway event */ - const startTyping = React.useCallback(() => { + const startTyping = React.useCallback(async () => { if (typeof typing === "number" && typing > +new Date()) return; logger.debug("ShouldStartTyping"); - // TODO: send typing request + await app.rest.post(Routes.channelTyping(channel.id)); setTyping(+new Date() + 10_000); }, [typing, setTyping]); @@ -113,21 +122,83 @@ function MessageInput({ channel }: Props) { return true; }, [uploadState, content]); - const send = React.useCallback(() => { - if (!canSendMessage()) return; - logger.debug("ShouldSendMessage"); + const sendMessage = React.useCallback(async () => { + stopTyping(); + const shouldFail = app.experiments.isTreatmentEnabled("message_queue", 2); + const shouldSend = !app.experiments.isTreatmentEnabled("message_queue", 1); + + if (!canSendMessage() && !shouldFail) return; + + const contentCopy = content; + const uploadStateCopy = { ...uploadState }; + + setContent(""); + setUploadState({ type: UploadStateType.NONE, files: [] }); + + const nonce = Snowflake.generate(); + const msg = app.queue.add({ + id: nonce, + content: contentCopy, + files: uploadState.files, + author: app.account!.raw, + channel: channel.id, + timestamp: new Date().toISOString(), + type: MessageType.Default, + }); + + if (shouldSend) { + try { + let body: RESTPostAPIChannelMessageJSONBody | FormData; + if (uploadStateCopy.files.length > 0) { + const data = new FormData(); + data.append("payload_json", JSON.stringify({ content, nonce })); + uploadStateCopy.files.forEach((file, index) => { + data.append(`files[${index}]`, file); + }); + body = data; + } else { + body = { content, nonce }; + } + await channel.sendMessage(body, msg); + } catch (e) { + const error = e instanceof Error ? e.message : typeof e === "string" ? e : "Unknown error"; + msg.fail(error); + } + } else { + msg.fail("Message queue experiment"); + } }, [content, uploadState, channel, canSendMessage]); const onKeyDown = (e: React.KeyboardEvent) => { - // TODO: - if (e.key === "Enter" && !e.shiftKey) { + if (e.ctrlKey && e.key === "Enter") { e.preventDefault(); - send(); + return sendMessage(); + } + + // TODO: handle editing last message + + if (!e.shiftKey && e.key === "Enter" && !isTouchscreenDevice) { + e.preventDefault(); + return sendMessage(); + } + + if (e.key === "Escape") { + if (uploadState.type === UploadStateType.ATTACHED && uploadState.files.length > 0) { + setUploadState({ + type: UploadStateType.NONE, + files: [], + }); + } } debouncedStopTyping(true); }; + const onChange = (e: React.ChangeEvent) => { + setContent(e.target.value); + startTyping(); + }; + return ( @@ -179,13 +250,8 @@ function MessageInput({ channel }: Props) { uploadState.type === UploadStateType.UPLOADING || uploadState.type === UploadStateType.SENDING } - onChange={(e) => { - setContent(e.target.value); - startTyping(); - }} - onKeyDown={() => { - debouncedStopTyping(); - }} + onChange={onChange} + onKeyDown={onKeyDown} /> {/* diff --git a/src/components/messaging/attachments/FileUpload.tsx b/src/components/messaging/attachments/AttachmentUpload.tsx similarity index 96% rename from src/components/messaging/attachments/FileUpload.tsx rename to src/components/messaging/attachments/AttachmentUpload.tsx index 3f471ef..73cdbf3 100644 --- a/src/components/messaging/attachments/FileUpload.tsx +++ b/src/components/messaging/attachments/AttachmentUpload.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react-lite"; import React from "react"; import styled from "styled-components"; import useLogger from "../../../hooks/useLogger"; @@ -102,4 +103,4 @@ function FileUpload({ append }: Props) { ); } -export default FileUpload; +export default observer(FileUpload); diff --git a/src/components/messaging/attachments/AttachmentUploadPreview.tsx b/src/components/messaging/attachments/AttachmentUploadPreview.tsx index 42f4c42..3fa187d 100644 --- a/src/components/messaging/attachments/AttachmentUploadPreview.tsx +++ b/src/components/messaging/attachments/AttachmentUploadPreview.tsx @@ -1,3 +1,4 @@ +import { observer } from "mobx-react-lite"; import React, { Fragment } from "react"; import styled from "styled-components"; import { bytesToSize } from "../../../utils/Utils"; @@ -160,4 +161,4 @@ function AttachmentUploadList({ state, remove }: Props) { ); } -export default AttachmentUploadList; +export default observer(AttachmentUploadList); diff --git a/src/stores/MessageQueue.ts b/src/stores/MessageQueue.ts index 8aab611..79f8b9b 100644 --- a/src/stores/MessageQueue.ts +++ b/src/stores/MessageQueue.ts @@ -41,13 +41,6 @@ export default class MessageQueue { message.status = QueuedMessageStatus.SENDING; } - @action - error(id: string, error: string) { - const message = this.messages.find((x) => x.id === id)!; - message.error = error; - message.status = QueuedMessageStatus.FAILED; - } - @computed get(channel: Snowflake) { return this.messages.filter((message) => message.channel === channel); diff --git a/src/stores/objects/QueuedMessage.ts b/src/stores/objects/QueuedMessage.ts index 1f56990..4f6857e 100644 --- a/src/stores/objects/QueuedMessage.ts +++ b/src/stores/objects/QueuedMessage.ts @@ -22,8 +22,8 @@ export default class QueuedMessage extends MessageBase { channel: string; files?: File[]; @observable progress = 0; - status: QueuedMessageStatus; - error?: string; + @observable status: QueuedMessageStatus; + @observable error?: string; abortCallback?: () => void; constructor(app: AppStore, data: QueuedMessageData) { @@ -37,8 +37,10 @@ export default class QueuedMessage extends MessageBase { @action updateProgress(e: ProgressEvent) { this.progress = Math.round((e.loaded / e.total) * 100); + console.log(this.progress); } + @action setAbortCallback(cb: () => void) { this.abortCallback = cb; } @@ -48,4 +50,13 @@ export default class QueuedMessage extends MessageBase { this.abortCallback(); } } + + @action + /** + * Mark this message as failed. + */ + fail(error: string) { + this.error = error; + this.status = QueuedMessageStatus.FAILED; + } }