diff --git a/web/chat/edit-message-provider.js b/web/chat/edit-message-provider.js
--- a/web/chat/edit-message-provider.js
+++ b/web/chat/edit-message-provider.js
@@ -19,6 +19,7 @@
   +clearEditModal: () => void,
   +editState: ?EditState,
   +setDraft: string => void,
+  +setError: boolean => void,
 };
 
 const EditModalContext: React.Context<EditModalContextType> =
@@ -27,6 +28,7 @@
     clearEditModal: () => {},
     editState: null,
     setDraft: () => {},
+    setError: () => {},
   });
 
 type Props = {
@@ -63,14 +65,27 @@
     [editState, setEditState],
   );
 
+  const setError = React.useCallback(
+    (isError: boolean) => {
+      if (!editState) {
+        return;
+      }
+      setEditState({
+        ...editState,
+        isError,
+      });
+    },
+    [editState, setEditState],
+  );
   const value = React.useMemo(
     () => ({
       renderEditModal,
       clearEditModal: clearEditModal,
       editState,
       setDraft,
+      setError,
     }),
-    [renderEditModal, clearEditModal, editState, setDraft],
+    [renderEditModal, clearEditModal, editState, setDraft, setError],
   );
 
   const modalOverlay = React.useMemo(() => {
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
@@ -4,6 +4,8 @@
 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 { type ThreadInfo } from 'lib/types/thread-types.js';
 
 import cssInputBar from './chat-input-bar.css';
@@ -26,8 +28,10 @@
 };
 
 function EditTextMessage(props: Props): React.Node {
-  const { background, threadInfo } = props;
-  const { editState, clearEditModal, setDraft } = useEditModalContext();
+  const { background, threadInfo, item } = props;
+  const { editState, clearEditModal, setDraft, setError } =
+    useEditModalContext();
+  const editMessage = useEditMessage();
 
   const editedMessageDraft = editState?.editedMessageDraft ?? '';
   const threadColor = threadInfo.color;
@@ -36,6 +40,36 @@
       backgroundColor: `#${threadColor}`,
     };
   }, [threadColor]);
+
+  const isMessageEdited = React.useMemo(() => {
+    const { messageInfo } = item;
+    if (!messageInfo || !messageInfo.text || !editState) {
+      return false;
+    }
+    if (!editedMessageDraft) {
+      return false;
+    }
+    const trimmedDraft = trimMessage(editedMessageDraft);
+    return trimmedDraft !== messageInfo.text;
+  }, [editState, editedMessageDraft, item]);
+
+  const checkAndEdit = async () => {
+    const { id: messageInfoID } = item.messageInfo;
+    if (!isMessageEdited) {
+      clearEditModal();
+      return;
+    }
+    if (!messageInfoID || !editState?.editedMessageDraft) {
+      return;
+    }
+    try {
+      await editMessage(messageInfoID, editState.editedMessageDraft);
+      clearEditModal();
+    } catch (e) {
+      setError(true);
+    }
+  };
+
   let editFailed;
   if (editState?.isError) {
     editFailed = (
@@ -56,6 +90,7 @@
           focus={!background}
           currentText={editedMessageDraft}
           setCurrentText={setDraft}
+          send={checkAndEdit}
         />
       </div>
       <div className={css.bottomRow}>
@@ -65,6 +100,7 @@
             className={[css.saveButton, css.smallButton]}
             variant="filled"
             buttonColor={saveButtonColor}
+            onClick={checkAndEdit}
           >
             Save (enter)
           </Button>