diff --git a/lib/shared/edit-messages-utils.js b/lib/shared/edit-messages-utils.js
--- a/lib/shared/edit-messages-utils.js
+++ b/lib/shared/edit-messages-utils.js
@@ -1,14 +1,54 @@
 // @flow
 
+import invariant from 'invariant';
+import * as React from 'react';
+
 import { threadHasPermission } from './thread-utils.js';
-import { messageTypes } from '../types/message-types.js';
+import {
+  sendEditMessageActionTypes,
+  sendEditMessage,
+} from '../actions/message-actions.js';
 import type {
+  SendEditMessageResult,
   RobotextMessageInfo,
   ComposableMessageInfo,
-} from '../types/message-types.js';
+} from '../types/message-types';
+import { messageTypes } from '../types/message-types.js';
 import { threadPermissions, type ThreadInfo } from '../types/thread-types.js';
+import {
+  useDispatchActionPromise,
+  useServerCall,
+} from '../utils/action-utils.js';
 import { useSelector } from '../utils/redux-utils.js';
 
+function useEditMessage(
+  messageID?: string,
+): (newText: string) => Promise<SendEditMessageResult> {
+  const callEditMessage = useServerCall(sendEditMessage);
+  const dispatchActionPromise = useDispatchActionPromise();
+
+  return React.useCallback(
+    newText => {
+      invariant(messageID, 'messageID should be set!');
+
+      const editMessagePromise = (async () => {
+        const result = await callEditMessage({
+          targetMessageID: messageID,
+          text: newText,
+        });
+        return {
+          newMessageInfos: result.newMessageInfos,
+        };
+      })();
+
+      dispatchActionPromise(sendEditMessageActionTypes, editMessagePromise);
+
+      return editMessagePromise;
+    },
+    [messageID, dispatchActionPromise, callEditMessage],
+  );
+}
+
 function useCanEditMessage(
   threadInfo: ThreadInfo,
   targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo,
@@ -36,4 +76,4 @@
   return hasPermission;
 }
 
-export { useCanEditMessage };
+export { useCanEditMessage, useEditMessage };