diff --git a/lib/hooks/sidebar-hooks.js b/lib/hooks/sidebar-hooks.js
--- a/lib/hooks/sidebar-hooks.js
+++ b/lib/hooks/sidebar-hooks.js
@@ -3,32 +3,18 @@
 import _orderBy from 'lodash/fp/orderBy.js';
 import * as React from 'react';
 
+import { useGetLastUpdatedTimes } from './thread-time.js';
 import { childThreadInfos } from '../selectors/thread-selectors.js';
 import { getMostRecentNonLocalMessageID } from '../shared/message-utils.js';
 import { threadInChatList } from '../shared/thread-utils.js';
-import type { MessageStore, RawMessageInfo } from '../types/message-types.js';
-import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
 import { threadTypeIsSidebar } from '../types/thread-types-enum.js';
 import type { SidebarInfo } from '../types/thread-types.js';
 import { useSelector } from '../utils/redux-utils.js';
 
-function getMostRecentRawMessageInfo(
-  threadInfo: ThreadInfo,
-  messageStore: MessageStore,
-): ?RawMessageInfo {
-  const thread = messageStore.threads[threadInfo.id];
-  if (!thread) {
-    return null;
-  }
-  for (const messageID of thread.messageIDs) {
-    return messageStore.messages[messageID];
-  }
-  return null;
-}
-
 function useSidebarInfos(): { +[id: string]: $ReadOnlyArray<SidebarInfo> } {
   const childThreadInfoByParentID = useSelector(childThreadInfos);
   const messageStore = useSelector(state => state.messageStore);
+  const getLastUpdatedTimes = useGetLastUpdatedTimes();
 
   return React.useMemo(() => {
     const result: { [id: string]: $ReadOnlyArray<SidebarInfo> } = {};
@@ -42,12 +28,11 @@
         ) {
           continue;
         }
-        const mostRecentRawMessageInfo = getMostRecentRawMessageInfo(
+        const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes(
           childThreadInfo,
           messageStore,
+          messageStore.messages,
         );
-        const lastUpdatedTime =
-          mostRecentRawMessageInfo?.time ?? childThreadInfo.creationTime;
         const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID(
           childThreadInfo.id,
           messageStore,
@@ -61,7 +46,7 @@
       result[parentID] = _orderBy('lastUpdatedTime')('desc')(sidebarInfos);
     }
     return result;
-  }, [childThreadInfoByParentID, messageStore]);
+  }, [childThreadInfoByParentID, messageStore, getLastUpdatedTimes]);
 }
 
 export { useSidebarInfos };
diff --git a/lib/hooks/thread-time.js b/lib/hooks/thread-time.js
--- a/lib/hooks/thread-time.js
+++ b/lib/hooks/thread-time.js
@@ -1,25 +1,107 @@
 // @flow
 
-import type { MessageInfo, MessageStore } from '../types/message-types.js';
+import * as React from 'react';
+
+import { useGetLatestMessageEdit } from './latest-message-edit.js';
+import { messageSpecs } from '../shared/messages/message-specs.js';
+import type {
+  MessageInfo,
+  RawMessageInfo,
+  MessageStore,
+} from '../types/message-types.js';
 import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js';
+import type { LastUpdatedTimes } from '../types/thread-types.js';
+import { useSelector } from '../utils/redux-utils.js';
 
-function getLastUpdatedTime(
+function useGetLastUpdatedTimes(): (
   threadInfo: ThreadInfo,
   messageStore: MessageStore,
-  messages: { +[id: string]: ?MessageInfo },
-): number {
-  const thread = messageStore.threads[threadInfo.id];
-  if (!thread) {
-    return threadInfo.creationTime;
-  }
-  for (const messageID of thread.messageIDs) {
-    const messageInfo = messages[messageID];
-    if (!messageInfo) {
-      continue;
-    }
-    return messageInfo.time;
-  }
-  return threadInfo.creationTime;
+  messages: { +[id: string]: ?MessageInfo | RawMessageInfo },
+) => LastUpdatedTimes {
+  const viewerID = useSelector(state => state.currentUserInfo?.id);
+  const fetchMessage = useGetLatestMessageEdit();
+  return React.useCallback(
+    (threadInfo, messageStore, messages) => {
+      // This callback returns two variables:
+      // - lastUpdatedTime: this is a Promise that resolves with the final value
+      // - lastUpdatedAtLeastTime: this is a number that represents what we
+      //   should use while we're waiting for lastUpdatedTime to resolve. It's
+      //   set based on the most recent message whose spec returns a non-Promise
+      //   when getLastUpdatedTime is called
+      let lastUpdatedAtLeastTime = threadInfo.creationTime;
+
+      const thread = messageStore.threads[threadInfo.id];
+      if (!thread || !viewerID) {
+        return {
+          lastUpdatedAtLeastTime,
+          lastUpdatedTime: Promise.resolve(lastUpdatedAtLeastTime),
+        };
+      }
+
+      const getLastUpdatedTimeParams = {
+        threadInfo,
+        viewerID,
+        fetchMessage,
+      };
+
+      let lastUpdatedTime: ?Promise<?number>;
+      for (const messageID of thread.messageIDs) {
+        const messageInfo = messages[messageID];
+        if (!messageInfo) {
+          continue;
+        }
+
+        // We call getLastUpdatedTime on the message spec. It can return either
+        // ?number or Promise<?number>. If the message spec doesn't implement
+        // getLastUpdatedTime, then we default to messageInfo.time.
+        const { getLastUpdatedTime } = messageSpecs[messageInfo.type];
+        const lastUpdatedTimePromisable = getLastUpdatedTime
+          ? getLastUpdatedTime(messageInfo, getLastUpdatedTimeParams)
+          : messageInfo.time;
+
+        // We rely on the fact that thread.messageIDs is ordered chronologically
+        // (newest first) to chain together lastUpdatedTime. An older message's
+        // lastUpdatedTime is only considered if all of the newer messages
+        // return falsey.
+        lastUpdatedTime = (async () => {
+          if (lastUpdatedTime) {
+            const earlierChecks = await lastUpdatedTime;
+            if (earlierChecks) {
+              return earlierChecks;
+            }
+          }
+          return await lastUpdatedTimePromisable;
+        })();
+
+        if (typeof lastUpdatedTimePromisable === 'number') {
+          // We break from the loop the first time this condition is met.
+          // There's no need to consider any older messages, since both
+          // lastUpdated and lastUpdatedAtLeastTime will be this value (or
+          // higher, in the case of lastUpdated). That is also why this loop
+          // only sets lastUpdatedAtLeastTime once: once we get to this
+          // "baseline" case, there's no need to consider any more messages.
+          lastUpdatedAtLeastTime = lastUpdatedTimePromisable;
+          break;
+        }
+      }
+
+      const lastUpdatedWithFallback = (async () => {
+        if (lastUpdatedTime) {
+          const earlierChecks = await lastUpdatedTime;
+          if (earlierChecks) {
+            return earlierChecks;
+          }
+        }
+        return lastUpdatedAtLeastTime;
+      })();
+
+      return {
+        lastUpdatedAtLeastTime,
+        lastUpdatedTime: lastUpdatedWithFallback,
+      };
+    },
+    [viewerID, fetchMessage],
+  );
 }
 
-export { getLastUpdatedTime };
+export { useGetLastUpdatedTimes };
diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js
--- a/lib/selectors/chat-selectors.js
+++ b/lib/selectors/chat-selectors.js
@@ -14,7 +14,7 @@
   threadInfoSelector,
 } from './thread-selectors.js';
 import { useSidebarInfos } from '../hooks/sidebar-hooks.js';
-import { getLastUpdatedTime } from '../hooks/thread-time.js';
+import { useGetLastUpdatedTimes } from '../hooks/thread-time.js';
 import {
   createMessageInfo,
   getMostRecentNonLocalMessageID,
@@ -97,13 +97,15 @@
   const messageInfos = useSelector(messageInfoSelector);
   const sidebarInfos = useSidebarInfos();
   const messageStore = useSelector(state => state.messageStore);
+  const getLastUpdatedTimes = useGetLastUpdatedTimes();
   return React.useCallback(
     threadInfo => {
       const mostRecentNonLocalMessage = getMostRecentNonLocalMessageID(
         threadInfo.id,
         messageStore,
       );
-      const lastUpdatedTime = getLastUpdatedTime(
+
+      const { lastUpdatedAtLeastTime: lastUpdatedTime } = getLastUpdatedTimes(
         threadInfo,
         messageStore,
         messageInfos,
@@ -170,7 +172,7 @@
         sidebars: sidebarItems,
       };
     },
-    [messageInfos, messageStore, sidebarInfos],
+    [messageInfos, messageStore, sidebarInfos, getLastUpdatedTimes],
   );
 }
 
diff --git a/lib/shared/messages/message-spec.js b/lib/shared/messages/message-spec.js
--- a/lib/shared/messages/message-spec.js
+++ b/lib/shared/messages/message-spec.js
@@ -141,4 +141,8 @@
     messageInfo: Info,
     params: ShowInMessagePreviewParams,
   ) => Promise<boolean>,
+  +getLastUpdatedTime?: (
+    messageInfoOrRawMessageInfo: Info | RawInfo,
+    params: ShowInMessagePreviewParams,
+  ) => ?number | Promise<?number>,
 };
diff --git a/lib/shared/messages/multimedia-message-spec.js b/lib/shared/messages/multimedia-message-spec.js
--- a/lib/shared/messages/multimedia-message-spec.js
+++ b/lib/shared/messages/multimedia-message-spec.js
@@ -369,6 +369,14 @@
 
   showInMessagePreview: (messageInfo: MediaMessageInfo | ImagesMessageInfo) =>
     Promise.resolve(messageInfo.media.length > 0),
+
+  getLastUpdatedTime: (
+    messageInfo:
+      | MediaMessageInfo
+      | ImagesMessageInfo
+      | RawMediaMessageInfo
+      | RawImagesMessageInfo,
+  ) => (messageInfo.media.length > 0 ? messageInfo.time : null),
 });
 
 // Four photos were uploaded before dimensions were calculated server-side,
diff --git a/lib/types/thread-types.js b/lib/types/thread-types.js
--- a/lib/types/thread-types.js
+++ b/lib/types/thread-types.js
@@ -436,6 +436,13 @@
   +offset: number,
 };
 
+export type LastUpdatedTimes = {
+  // The last updated time is at least this number, but possibly higher
+  // We won't know for sure until the below Promise resolves
+  +lastUpdatedAtLeastTime: number,
+  +lastUpdatedTime: Promise<number>,
+};
+
 export type SidebarInfo = {
   +threadInfo: ThreadInfo,
   +lastUpdatedTime: number,