diff --git a/lib/push/send-hooks.react.js b/lib/push/send-hooks.react.js
new file mode 100644
--- /dev/null
+++ b/lib/push/send-hooks.react.js
@@ -0,0 +1,51 @@
+// @flow
+
+import * as React from 'react';
+
+import { preparePushNotifs } from './send-utils.js';
+import { ENSCacheContext } from '../components/ens-cache-provider.react.js';
+import { NeynarClientContext } from '../components/neynar-client-provider.react.js';
+import type { MessageData, RawMessageInfo } from '../types/message-types.js';
+import type { ResolvedNotifTexts } from '../types/notif-types.js';
+import { useSelector } from '../utils/redux-utils.js';
+
+function usePreparePushNotifs(): (
+  messageDatas: $ReadOnlyArray<MessageData>,
+) => Promise<?{
+  +[userID: string]: $ReadOnlyArray<{
+    +notifTexts: ResolvedNotifTexts,
+    +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+  }>,
+}> {
+  const rawMessageInfos = useSelector(state => state.messageStore.messages);
+  const rawThreadInfos = useSelector(state => state.threadStore.threadInfos);
+  const auxUserInfos = useSelector(state => state.auxUserStore.auxUserInfos);
+  const userInfos = useSelector(state => state.userStore.userInfos);
+
+  const { getENSNames } = React.useContext(ENSCacheContext);
+  const getFCNames = React.useContext(NeynarClientContext)?.getFCNames;
+
+  return React.useCallback(
+    (messageDatas: $ReadOnlyArray<MessageData>) => {
+      return preparePushNotifs(
+        rawMessageInfos,
+        rawThreadInfos,
+        auxUserInfos,
+        messageDatas,
+        userInfos,
+        getENSNames,
+        getFCNames,
+      );
+    },
+    [
+      rawMessageInfos,
+      rawThreadInfos,
+      auxUserInfos,
+      userInfos,
+      getENSNames,
+      getFCNames,
+    ],
+  );
+}
+
+export { usePreparePushNotifs };
diff --git a/lib/push/send-utils.js b/lib/push/send-utils.js
--- a/lib/push/send-utils.js
+++ b/lib/push/send-utils.js
@@ -5,10 +5,17 @@
 import uuidv4 from 'uuid/v4.js';
 
 import { hasPermission } from '../permissions/minimally-encoded-thread-permissions.js';
-import { rawMessageInfoFromMessageData } from '../shared/message-utils.js';
+import {
+  rawMessageInfoFromMessageData,
+  createMessageInfo,
+} from '../shared/message-utils.js';
 import { type PushType, pushTypes } from '../shared/messages/message-spec.js';
 import { messageSpecs } from '../shared/messages/message-specs.js';
-import { isMemberActive } from '../shared/thread-utils.js';
+import { notifTextsForMessageInfo } from '../shared/notif-utils.js';
+import {
+  isMemberActive,
+  threadInfoFromRawThreadInfo,
+} from '../shared/thread-utils.js';
 import type { AuxUserInfos } from '../types/aux-user-types.js';
 import type { PlatformDetails } from '../types/device-types.js';
 import {
@@ -20,7 +27,12 @@
   type RawMessageInfo,
   messageDataLocalID,
 } from '../types/message-types.js';
+import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
+import type { ResolvedNotifTexts } from '../types/notif-types.js';
 import type { RawThreadInfos } from '../types/thread-types.js';
+import type { UserInfos } from '../types/user-types.js';
+import { type GetENSNames } from '../utils/ens-helpers.js';
+import { type GetFCNames } from '../utils/farcaster-helpers.js';
 import { promiseAll } from '../utils/promises.js';
 
 type PushUserInfo = {
@@ -223,4 +235,181 @@
   };
 }
 
-export { getPushUserInfo };
+async function buildNotifText(
+  inputData: {
+    +rawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+    +userID: string,
+    +threadInfos: { +[id: string]: ThreadInfo },
+    +userInfos: UserInfos,
+  },
+  getENSNames: ?GetENSNames,
+  getFCNames: ?GetFCNames,
+): Promise<?{
+  +notifTexts: ResolvedNotifTexts,
+  +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+}> {
+  const { rawMessageInfos, userID, threadInfos, userInfos } = inputData;
+  const hydrateMessageInfo = (rawMessageInfo: RawMessageInfo) =>
+    createMessageInfo(rawMessageInfo, userID, userInfos, threadInfos);
+
+  const newMessageInfos = [];
+  const newRawMessageInfos = [];
+
+  for (const rawMessageInfo of rawMessageInfos) {
+    const newMessageInfo = hydrateMessageInfo(rawMessageInfo);
+    if (newMessageInfo) {
+      newMessageInfos.push(newMessageInfo);
+      newRawMessageInfos.push(rawMessageInfo);
+    }
+  }
+
+  if (newMessageInfos.length === 0) {
+    return null;
+  }
+
+  const [{ threadID }] = newMessageInfos;
+  const threadInfo = threadInfos[threadID];
+  const parentThreadInfo = threadInfo.parentThreadID
+    ? threadInfos[threadInfo.parentThreadID]
+    : null;
+
+  // TODO: Using types from @Ashoat check ThreadSubscription and mentioning
+
+  const username = userInfos[userID] && userInfos[userID].username;
+  const notifTargetUserInfo = { id: userID, username };
+  const notifTexts = await notifTextsForMessageInfo(
+    newMessageInfos,
+    threadInfo,
+    parentThreadInfo,
+    notifTargetUserInfo,
+    getENSNames,
+    getFCNames,
+  );
+  if (!notifTexts) {
+    return null;
+  }
+
+  return { notifTexts, newRawMessageInfos };
+}
+
+async function buildNotifsTexts(
+  pushInfo: PushInfo,
+  rawThreadInfos: RawThreadInfos,
+  userInfos: UserInfos,
+  getENSNames: ?GetENSNames,
+  getFCNames: ?GetFCNames,
+): Promise<{
+  +[userID: string]: $ReadOnlyArray<{
+    +notifTexts: ResolvedNotifTexts,
+    +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+  }>,
+}> {
+  const threadIDs = new Set<string>();
+
+  for (const userID in pushInfo) {
+    for (const rawMessageInfo of pushInfo[userID].messageInfos) {
+      const threadID = rawMessageInfo.threadID;
+      threadIDs.add(threadID);
+
+      const messageSpec = messageSpecs[rawMessageInfo.type];
+      if (messageSpec.threadIDs) {
+        for (const id of messageSpec.threadIDs(rawMessageInfo)) {
+          threadIDs.add(id);
+        }
+      }
+    }
+  }
+
+  const perUserNotifTextsPromises: {
+    [userID: string]: Promise<
+      Array<?{
+        +notifTexts: ResolvedNotifTexts,
+        +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+      }>,
+    >,
+  } = {};
+
+  for (const userID in pushInfo) {
+    const threadInfos = Object.fromEntries(
+      [...threadIDs].map(threadID => [
+        threadID,
+        threadInfoFromRawThreadInfo(
+          rawThreadInfos[threadID],
+          userID,
+          userInfos,
+        ),
+      ]),
+    );
+
+    const userNotifTextsPromises = [];
+
+    for (const rawMessageInfos of pushInfo[userID].messageInfos) {
+      userNotifTextsPromises.push(
+        buildNotifText(
+          {
+            // This is because coalescing is not supported
+            // for notifications generated on the client
+            rawMessageInfos: [rawMessageInfos],
+            threadInfos,
+            userID,
+            userInfos,
+          },
+          getENSNames,
+          getFCNames,
+        ),
+      );
+    }
+
+    perUserNotifTextsPromises[userID] = Promise.all(userNotifTextsPromises);
+  }
+
+  const perUserNotifTexts = await promiseAll(perUserNotifTextsPromises);
+  const filteredPerUserNotifTexts: {
+    [userID: string]: $ReadOnlyArray<{
+      +notifTexts: ResolvedNotifTexts,
+      +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+    }>,
+  } = {};
+
+  for (const userID in perUserNotifTexts) {
+    filteredPerUserNotifTexts[userID] =
+      perUserNotifTexts[userID].filter(Boolean);
+  }
+  return filteredPerUserNotifTexts;
+}
+
+async function preparePushNotifs(
+  messageInfos: { +[id: string]: RawMessageInfo },
+  rawThreadInfos: RawThreadInfos,
+  auxUserInfos: AuxUserInfos,
+  messageDatas: $ReadOnlyArray<MessageData>,
+  userInfos: UserInfos,
+  getENSNames: ?GetENSNames,
+  getFCNames: ?GetFCNames,
+): Promise<?{
+  +[userID: string]: $ReadOnlyArray<{
+    +notifTexts: ResolvedNotifTexts,
+    +newRawMessageInfos: $ReadOnlyArray<RawMessageInfo>,
+  }>,
+}> {
+  const { pushInfos } = await getPushUserInfo(
+    messageInfos,
+    rawThreadInfos,
+    auxUserInfos,
+    messageDatas,
+  );
+
+  if (!pushInfos) {
+    return null;
+  }
+
+  return await buildNotifsTexts(
+    pushInfos,
+    rawThreadInfos,
+    userInfos,
+    getENSNames,
+    getFCNames,
+  );
+}
+
+export { preparePushNotifs };