diff --git a/native/chat/chat-thread-list-utils.js b/native/chat/chat-thread-list-utils.js
new file mode 100644
--- /dev/null
+++ b/native/chat/chat-thread-list-utils.js
@@ -0,0 +1,58 @@
+// @flow
+
+import _sum from 'lodash/fp/sum.js';
+import { Platform } from 'react-native';
+
+import {
+  chatThreadListItemHeight,
+  spacerHeight,
+} from './chat-thread-list-item.react.js';
+import type { Item } from './chat-thread-list.react.js';
+import { sidebarHeight } from './sidebar-item.react.js';
+
+function keyExtractor(item: Item): string {
+  if (item.type === 'chatThreadItem') {
+    return item.threadInfo.id;
+  } else if (item.type === 'empty') {
+    return 'empty';
+  }
+  return 'search';
+}
+
+function itemHeight(item: Item): number {
+  if (item.type === 'search') {
+    return Platform.OS === 'ios' ? 54.5 : 55;
+  }
+
+  // itemHeight for emptyItem might be wrong because of line wrapping
+  // but we don't care because we'll only ever be rendering this item
+  // by itself and it should always be on-screen
+  if (item.type === 'empty') {
+    return 123;
+  }
+
+  const baseHeight = chatThreadListItemHeight;
+  const sidebarsHeight = item.sidebars.length * sidebarHeight;
+  const spacerHeightAdjustment = item.sidebars.length > 0 ? spacerHeight : 0;
+
+  return baseHeight + sidebarsHeight + spacerHeightAdjustment;
+}
+
+function heightOfItems(data: $ReadOnlyArray<Item>): number {
+  return _sum(data.map(itemHeight));
+}
+
+function getItemLayout(
+  data: ?$ReadOnlyArray<Item>,
+  index: number,
+): { length: number, offset: number, index: number } {
+  if (!data) {
+    return { length: 0, offset: 0, index };
+  }
+  const offset = heightOfItems(data.filter((_, i): boolean => i < index));
+  const item = data[index];
+  const length = item ? itemHeight(item) : 0;
+  return { length, offset, index };
+}
+
+export { keyExtractor, itemHeight, heightOfItems, getItemLayout };
diff --git a/native/chat/chat-thread-list.react.js b/native/chat/chat-thread-list.react.js
--- a/native/chat/chat-thread-list.react.js
+++ b/native/chat/chat-thread-list.react.js
@@ -2,7 +2,6 @@
 
 import IonIcon from '@expo/vector-icons/Ionicons.js';
 import invariant from 'invariant';
-import _sum from 'lodash/fp/sum.js';
 import * as React from 'react';
 import {
   View,
@@ -39,11 +38,8 @@
 } from 'lib/types/user-types.js';
 import { useServerCall } from 'lib/utils/action-utils.js';
 
-import {
-  ChatThreadListItem,
-  chatThreadListItemHeight,
-  spacerHeight,
-} from './chat-thread-list-item.react.js';
+import { ChatThreadListItem } from './chat-thread-list-item.react.js';
+import { getItemLayout, keyExtractor } from './chat-thread-list-utils.js';
 import type {
   ChatTopTabsNavigationProp,
   ChatNavigationProp,
@@ -52,7 +48,6 @@
   type MessageListParams,
   useNavigateToThread,
 } from './message-list-types.js';
-import { sidebarHeight } from './sidebar-item.react.js';
 import Button from '../components/button.react.js';
 import Search from '../components/search.react.js';
 import {
@@ -85,7 +80,7 @@
 const { Value, Node, interpolateNode, useValue } = Animated;
 /* eslint-enable import/no-named-as-default-member */
 
-type Item =
+export type Item =
   | ChatThreadItem
   | { +type: 'search', +searchText: string }
   | { +type: 'empty', +emptyItem: React.ComponentType<{}> };
@@ -326,52 +321,6 @@
     );
   };
 
-  static keyExtractor = (item: Item) => {
-    if (item.type === 'chatThreadItem') {
-      return item.threadInfo.id;
-    } else if (item.type === 'empty') {
-      return 'empty';
-    } else {
-      return 'search';
-    }
-  };
-
-  static getItemLayout = (data: ?$ReadOnlyArray<Item>, index: number) => {
-    if (!data) {
-      return { length: 0, offset: 0, index };
-    }
-    const offset = ChatThreadList.heightOfItems(
-      data.filter((_, i) => i < index),
-    );
-    const item = data[index];
-    const length = item ? ChatThreadList.itemHeight(item) : 0;
-    return { length, offset, index };
-  };
-
-  static itemHeight = (item: Item) => {
-    if (item.type === 'search') {
-      return Platform.OS === 'ios' ? 54.5 : 55;
-    }
-
-    // itemHeight for emptyItem might be wrong because of line wrapping
-    // but we don't care because we'll only ever be rendering this item
-    // by itself and it should always be on-screen
-    if (item.type === 'empty') {
-      return 123;
-    }
-
-    let height = chatThreadListItemHeight;
-    height += item.sidebars.length * sidebarHeight;
-    if (item.sidebars.length > 0) {
-      height += spacerHeight;
-    }
-    return height;
-  };
-
-  static heightOfItems(data: $ReadOnlyArray<Item>): number {
-    return _sum(data.map(ChatThreadList.itemHeight));
-  }
-
   listDataSelector = createSelector(
     (props: Props) => props.chatListData,
     (props: Props) => props.searchStatus,
@@ -465,8 +414,8 @@
         <FlatList
           data={this.listData}
           renderItem={this.renderItem}
-          keyExtractor={ChatThreadList.keyExtractor}
-          getItemLayout={ChatThreadList.getItemLayout}
+          keyExtractor={keyExtractor}
+          getItemLayout={getItemLayout}
           extraData={extraData}
           initialNumToRender={11}
           keyboardShouldPersistTaps="handled"