diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js
index af543559f..c4891e252 100644
--- a/lib/utils/entity-text.js
+++ b/lib/utils/entity-text.js
@@ -1,194 +1,304 @@
 // @flow
 
 import invariant from 'invariant';
 import * as React from 'react';
 
 import { threadNoun } from '../shared/thread-utils';
 import { stringForUser } from '../shared/user-utils';
-import { threadTypes, type ThreadType } from '../types/thread-types';
+import {
+  threadTypes,
+  type ThreadType,
+  type RawThreadInfo,
+  type ThreadInfo,
+} from '../types/thread-types';
 
 type UserEntity = {
   +type: 'user',
   +id: string,
   +username?: ?string,
   +isViewer?: ?boolean,
   +possessive?: ?boolean, // eg. `user's` instead of `user`
 };
 
 // Comments explain how thread name will appear from user4's perspective
 type ThreadEntity =
   | {
       +type: 'thread',
       +id: string,
       +name?: ?string,
       // displays threadInfo.name if set, or 'user1, user2, and user3'
       +display: 'uiName',
       +uiName: string,
     }
   | {
       +type: 'thread',
       +id: string,
       +name?: ?string,
       // displays threadInfo.name if set, or eg. 'this thread' or 'this chat'
       +display: 'shortName',
       +threadType?: ?ThreadType,
       +alwaysDisplayShortName?: ?boolean, // don't default to name
       +possessive?: ?boolean, // eg. `this thread's` instead of `this thread`
     };
 
 type ColorEntity = {
   +type: 'color',
   +hex: string,
 };
 
 type EntityTextComponent = UserEntity | ThreadEntity | ColorEntity | string;
 
 export type EntityText = $ReadOnlyArray<EntityTextComponent>;
 
-// ET is a JS tag function used in template literals, eg. ET`something`
-// It allows you to compose raw text and "entities" together
-function ET(
+const entityTextFunction = (
   strings: $ReadOnlyArray<string>,
   ...entities: $ReadOnlyArray<EntityTextComponent | EntityText>
-): EntityText {
+) => {
   const result = [];
   for (let i = 0; i < strings.length; i++) {
     const str = strings[i];
     if (str) {
       result.push(str);
     }
     const entity = entities[i];
     if (!entity) {
       continue;
     }
 
     if (typeof entity === 'string') {
       const lastResult = result.length > 0 && result[result.length - 1];
       if (typeof lastResult === 'string') {
         result[result.length - 1] = lastResult + entity;
       } else {
         result.push(entity);
       }
     } else if (Array.isArray(entity)) {
       const [firstEntity, ...restOfEntity] = entity;
       const lastResult = result.length > 0 && result[result.length - 1];
       if (typeof lastResult === 'string' && typeof firstEntity === 'string') {
         result[result.length - 1] = lastResult + firstEntity;
       } else if (firstEntity) {
         result.push(firstEntity);
       }
       result.push(...restOfEntity);
     } else {
       result.push(entity);
     }
   }
   return result;
-}
+};
+
+// defaults to shortName
+type EntityTextThreadInput =
+  | {
+      +display: 'uiName',
+      +threadInfo: ThreadInfo,
+    }
+  | {
+      +display?: 'shortName',
+      +threadInfo: ThreadInfo | RawThreadInfo,
+      +possessive?: ?boolean,
+    }
+  | {
+      +display: 'alwaysDisplayShortName',
+      +threadInfo: ThreadInfo | RawThreadInfo,
+      +possessive?: ?boolean,
+    }
+  | {
+      +display: 'alwaysDisplayShortName',
+      +threadID: string,
+      +possessive?: ?boolean,
+    };
+entityTextFunction.thread = (input: EntityTextThreadInput) => {
+  if (input.display === 'uiName') {
+    const { threadInfo } = input;
+    return {
+      type: 'thread',
+      id: threadInfo.id,
+      name: threadInfo.name,
+      display: 'uiName',
+      uiName: threadInfo.uiName,
+    };
+  }
+  if (input.display === 'alwaysDisplayShortName' && input.threadID) {
+    const { threadID, possessive } = input;
+    return {
+      type: 'thread',
+      id: threadID,
+      name: undefined,
+      display: 'shortName',
+      threadType: undefined,
+      alwaysDisplayShortName: true,
+      possessive,
+    };
+  } else if (input.display === 'alwaysDisplayShortName' && input.threadInfo) {
+    const { threadInfo, possessive } = input;
+    return {
+      type: 'thread',
+      id: threadInfo.id,
+      name: threadInfo.name,
+      display: 'shortName',
+      threadType: threadInfo.type,
+      alwaysDisplayShortName: true,
+      possessive,
+    };
+  } else if (input.display === 'shortName' || !input.display) {
+    const { threadInfo, possessive } = input;
+    return {
+      type: 'thread',
+      id: threadInfo.id,
+      name: threadInfo.name,
+      display: 'shortName',
+      threadType: threadInfo.type,
+      possessive,
+    };
+  }
+  invariant(
+    false,
+    `ET.thread passed unexpected display type: ${input.display}`,
+  );
+};
+
+type EntityTextUserInput = {
+  +userInfo: {
+    +id: string,
+    +username?: ?string,
+    +isViewer?: ?boolean,
+    ...
+  },
+  +possessive?: ?boolean,
+};
+entityTextFunction.user = (input: EntityTextUserInput) => ({
+  type: 'user',
+  id: input.userInfo.id,
+  username: input.userInfo.username,
+  isViewer: input.userInfo.isViewer,
+  possessive: input.possessive,
+});
+
+type EntityTextColorInput = { +hex: string };
+entityTextFunction.color = (input: EntityTextColorInput) => ({
+  type: 'color',
+  hex: input.hex,
+});
+
+// ET is a JS tag function used in template literals, eg. ET`something`
+// It allows you to compose raw text and "entities" together
+type EntityTextFunction = ((
+  strings: $ReadOnlyArray<string>,
+  ...entities: $ReadOnlyArray<EntityTextComponent | EntityText>
+) => EntityText) & {
+  +thread: EntityTextThreadInput => ThreadEntity,
+  +user: EntityTextUserInput => UserEntity,
+  +color: EntityTextColorInput => ColorEntity,
+  ...
+};
+const ET: EntityTextFunction = entityTextFunction;
 
 type MakePossessiveInput = { +str: string, +isViewer?: ?boolean };
 function makePossessive(input: MakePossessiveInput) {
   if (input.isViewer) {
     return 'your';
   }
   return `${input.str}’s`;
 }
 
 function getNameForThreadEntity(entity: ThreadEntity): string {
   const { name: userGeneratedName, display } = entity;
   if (entity.display === 'uiName') {
     return userGeneratedName ? userGeneratedName : entity.uiName;
   }
   invariant(
     entity.display === 'shortName',
     `getNameForThreadEntity can't handle thread entity display ${display}`,
   );
 
   let name = userGeneratedName;
   if (!name || entity.alwaysDisplayShortName) {
     const threadType = entity.threadType ?? threadTypes.PERSONAL;
     name = `this ${threadNoun(threadType)}`;
   }
   if (entity.possessive) {
     name = makePossessive({ str: name });
   }
   return name;
 }
 
 function getNameForUserEntity(entity: UserEntity): string {
   const str = stringForUser(entity);
   const { isViewer, possessive } = entity;
   if (!possessive) {
     return str;
   }
   return makePossessive({ str, isViewer });
 }
 
 function entityTextToRawString(entityText: EntityText): string {
   const textParts = entityText.map(entity => {
     if (typeof entity === 'string') {
       return entity;
     } else if (entity.type === 'thread') {
       return getNameForThreadEntity(entity);
     } else if (entity.type === 'color') {
       return entity.hex;
     } else if (entity.type === 'user') {
       return getNameForUserEntity(entity);
     } else {
       invariant(
         false,
         `entityTextToRawString can't handle entity type ${entity.type}`,
       );
     }
   });
   return textParts.join('');
 }
 
 type RenderFunctions = {
   +renderText: ({ +text: string }) => React.Node,
   +renderThread: ({ +id: string, +name: string }) => React.Node,
   +renderColor: ({ +hex: string }) => React.Node,
 };
 function entityTextToReact(
   entityText: EntityText,
   threadID: string,
   renderFuncs: RenderFunctions,
 ): React.Node {
   const { renderText, renderThread, renderColor } = renderFuncs;
   return entityText.map((entity, i) => {
     const key = `text${i}`;
     if (typeof entity === 'string') {
       return (
         <React.Fragment key={key}>
           {renderText({ text: entity })}
         </React.Fragment>
       );
     } else if (entity.type === 'thread') {
       const { id } = entity;
       const name = getNameForThreadEntity(entity);
       if (id === threadID) {
         return name;
       } else {
         return (
           <React.Fragment key={key}>
             {renderThread({ id, name })}
           </React.Fragment>
         );
       }
     } else if (entity.type === 'color') {
       return (
         <React.Fragment key={key}>
           {renderColor({ hex: entity.hex })}
         </React.Fragment>
       );
     } else if (entity.type === 'user') {
       return getNameForUserEntity(entity);
     } else {
       invariant(
         false,
         `entityTextToReact can't handle entity type ${entity.type}`,
       );
     }
   });
 }
 
 export { ET, entityTextToRawString, entityTextToReact };