diff --git a/web/chat/edit-text-message.react.js b/web/chat/edit-text-message.react.js
--- a/web/chat/edit-text-message.react.js
+++ b/web/chat/edit-text-message.react.js
@@ -5,7 +5,10 @@
 import { XCircle as XCircleIcon } from 'react-feather';
 
 import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
+import { useEditMessage } from 'lib/shared/edit-messages-utils.js';
+import { trimMessage } from 'lib/shared/message-utils.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
+import type { SendEditMessageResult } from 'lib/types/message-types.js';
 import { type ThreadInfo } from 'lib/types/thread-types.js';
 
 import cssInputBar from './chat-input-bar.css';
@@ -26,6 +29,10 @@
   ...BaseProps,
   +editState: ?EditState,
   +clearEditModal: () => void,
+  +editMessage: (
+    messageID: string,
+    text: string,
+  ) => Promise<SendEditMessageResult>,
   +setDraft: string => void,
 };
 class EditTextMessage extends React.PureComponent<Props> {
@@ -70,6 +77,7 @@
       >
         <div className={cssInputBar.inputBarTextInput}>
           <ChatInputTextArea
+            send={this.editMessage}
             focus={!this.props.background}
             currentText={editedMessageDraft}
             setCurrentText={this.props.setDraft}
@@ -79,7 +87,11 @@
           {editFailed}
           <div className={css.buttons}>
             <button onClick={this.exitEditMode}>Cancel (esc)</button>
-            <button className={css.saveButton} style={messageStyle}>
+            <button
+              className={css.saveButton}
+              style={messageStyle}
+              onClick={this.editMessage}
+            >
               Save (enter)
             </button>
           </div>
@@ -88,6 +100,39 @@
     );
   }
 
+  isMessageEdited = () => {
+    const { messageInfo } = this.props.item;
+    if (!messageInfo || !messageInfo.text || !this.props.editState) {
+      return false;
+    }
+    const { editedMessageDraft } = this.props.editState;
+    if (!editedMessageDraft) {
+      return false;
+    }
+    const trimmedDraft = trimMessage(editedMessageDraft);
+    return trimmedDraft !== messageInfo.text;
+  };
+
+  editMessage = async () => {
+    const { id: messageInfoID } = this.props.item.messageInfo;
+    if (!this.isMessageEdited()) {
+      this.exitEditMode();
+      return;
+    }
+    if (!messageInfoID || !this.props.editState?.editedMessageDraft) {
+      return;
+    }
+    try {
+      await this.props.editMessage(
+        messageInfoID,
+        this.props.editState.editedMessageDraft,
+      );
+      this.exitEditMode();
+    } catch (e) {
+      // TODO: Handle error
+    }
+  };
+
   exitEditMode = () => {
     this.props.clearEditModal();
   };
@@ -96,11 +141,13 @@
 const ConnectedEditTextMessage: React.ComponentType<BaseProps> =
   React.memo<BaseProps>(function ConnectedEditTextMessage(props) {
     const { editState, clearEditModal, setDraft } = useEditModalContext();
+    const editMessage = useEditMessage();
     return (
       <EditTextMessage
         {...props}
         editState={editState}
         clearEditModal={clearEditModal}
+        editMessage={editMessage}
         setDraft={setDraft}
       />
     );