diff --git a/lib/components/chat-mention-provider.react.js b/lib/components/chat-mention-provider.react.js
index 301c8c987..2daededbd 100644
--- a/lib/components/chat-mention-provider.react.js
+++ b/lib/components/chat-mention-provider.react.js
@@ -1,227 +1,236 @@
// @flow
import * as React from 'react';
import genesis from '../facts/genesis.js';
import { threadInfoSelector } from '../selectors/thread-selectors.js';
import SentencePrefixSearchIndex from '../shared/sentence-prefix-search-index.js';
import { threadTypes } from '../types/thread-types-enum.js';
import type {
ChatMentionCandidates,
ChatMentionCandidatesObj,
ResolvedThreadInfo,
ThreadInfo,
} from '../types/thread-types.js';
import { useResolvedThreadInfosObj } from '../utils/entity-helpers.js';
import { useSelector } from '../utils/redux-utils.js';
type Props = {
+children: React.Node,
};
export type ChatMentionContextType = {
+getChatMentionSearchIndex: (
threadInfo: ThreadInfo,
) => SentencePrefixSearchIndex,
+communityThreadIDForGenesisThreads: { +[id: string]: string },
+chatMentionCandidatesObj: ChatMentionCandidatesObj,
};
const emptySearchIndex = new SentencePrefixSearchIndex();
const ChatMentionContext: React.Context =
React.createContext({
getChatMentionSearchIndex: () => emptySearchIndex,
communityThreadIDForGenesisThreads: {},
chatMentionCandidatesObj: {},
});
function ChatMentionContextProvider(props: Props): React.Node {
const { children } = props;
const { communityThreadIDForGenesisThreads, chatMentionCandidatesObj } =
useChatMentionCandidatesObjAndUtils();
const searchIndices = useChatMentionSearchIndex(chatMentionCandidatesObj);
const getChatMentionSearchIndex = React.useCallback(
(threadInfo: ThreadInfo) => {
if (threadInfo.community === genesis.id) {
return searchIndices[communityThreadIDForGenesisThreads[threadInfo.id]];
}
return searchIndices[threadInfo.community ?? threadInfo.id];
},
[communityThreadIDForGenesisThreads, searchIndices],
);
const value = React.useMemo(
() => ({
getChatMentionSearchIndex,
communityThreadIDForGenesisThreads,
chatMentionCandidatesObj,
}),
[
getChatMentionSearchIndex,
communityThreadIDForGenesisThreads,
chatMentionCandidatesObj,
],
);
return (
{children}
);
}
function getChatMentionCandidates(threadInfos: {
+[id: string]: ResolvedThreadInfo,
}): {
chatMentionCandidatesObj: ChatMentionCandidatesObj,
communityThreadIDForGenesisThreads: { +[id: string]: string },
} {
const result = {};
const visitedGenesisThreads = new Set();
const communityThreadIDForGenesisThreads = {};
for (const currentThreadID in threadInfos) {
const currentThreadInfo = threadInfos[currentThreadID];
const { community: currentThreadCommunity } = currentThreadInfo;
if (!currentThreadCommunity) {
if (!result[currentThreadID]) {
result[currentThreadID] = { [currentThreadID]: currentThreadInfo };
}
continue;
}
if (!result[currentThreadCommunity]) {
result[currentThreadCommunity] = {
[currentThreadCommunity]: threadInfos[currentThreadCommunity],
};
}
// Handle GENESIS community case: mentioning inside GENESIS should only
// show chats and threads inside the top level that is below GENESIS.
if (threadInfos[currentThreadCommunity].type === threadTypes.GENESIS) {
if (visitedGenesisThreads.has(currentThreadID)) {
continue;
}
const threadTraversePath = [currentThreadInfo];
visitedGenesisThreads.add(currentThreadID);
let currentlySelectedThreadID = currentThreadInfo.parentThreadID;
while (currentlySelectedThreadID) {
const currentlySelectedThreadInfo =
threadInfos[currentlySelectedThreadID];
if (
visitedGenesisThreads.has(currentlySelectedThreadID) ||
!currentlySelectedThreadInfo ||
currentlySelectedThreadInfo.type === threadTypes.GENESIS
) {
break;
}
threadTraversePath.push(currentlySelectedThreadInfo);
visitedGenesisThreads.add(currentlySelectedThreadID);
currentlySelectedThreadID = currentlySelectedThreadInfo.parentThreadID;
}
const lastThreadInTraversePath =
threadTraversePath[threadTraversePath.length - 1];
let lastThreadInTraversePathParentID;
if (lastThreadInTraversePath.parentThreadID) {
lastThreadInTraversePathParentID = threadInfos[
lastThreadInTraversePath.parentThreadID
]
? lastThreadInTraversePath.parentThreadID
: lastThreadInTraversePath.id;
} else {
lastThreadInTraversePathParentID = lastThreadInTraversePath.id;
}
if (
threadInfos[lastThreadInTraversePathParentID].type ===
threadTypes.GENESIS
) {
if (!result[lastThreadInTraversePath.id]) {
result[lastThreadInTraversePath.id] = {};
}
for (const threadInfo of threadTraversePath) {
result[lastThreadInTraversePath.id][threadInfo.id] = threadInfo;
communityThreadIDForGenesisThreads[threadInfo.id] =
lastThreadInTraversePath.id;
}
if (
lastThreadInTraversePath.type !== threadTypes.PERSONAL &&
lastThreadInTraversePath.type !== threadTypes.PRIVATE
) {
result[genesis.id][lastThreadInTraversePath.id] =
lastThreadInTraversePath;
}
} else {
if (
!communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID]
) {
result[lastThreadInTraversePathParentID] = {};
communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID] =
lastThreadInTraversePathParentID;
}
const lastThreadInTraversePathParentCommunityThreadID =
communityThreadIDForGenesisThreads[lastThreadInTraversePathParentID];
for (const threadInfo of threadTraversePath) {
result[lastThreadInTraversePathParentCommunityThreadID][
threadInfo.id
] = threadInfo;
communityThreadIDForGenesisThreads[threadInfo.id] =
lastThreadInTraversePathParentCommunityThreadID;
}
}
continue;
}
result[currentThreadCommunity][currentThreadID] = currentThreadInfo;
}
return {
chatMentionCandidatesObj: result,
communityThreadIDForGenesisThreads,
};
}
+// Without allAtOnce, useChatMentionCandidatesObjAndUtils is very expensive.
+// useResolvedThreadInfosObj would trigger its recalculation for each ENS name
+// as it streams in, but we would prefer to trigger its recaculation just once
+// for every update of the underlying Redux data.
+const useResolvedThreadInfosObjOptions = { allAtOnce: true };
+
function useChatMentionCandidatesObjAndUtils(): {
chatMentionCandidatesObj: ChatMentionCandidatesObj,
resolvedThreadInfos: ChatMentionCandidates,
communityThreadIDForGenesisThreads: { +[id: string]: string },
} {
const threadInfos = useSelector(threadInfoSelector);
- const resolvedThreadInfos = useResolvedThreadInfosObj(threadInfos);
+ const resolvedThreadInfos = useResolvedThreadInfosObj(
+ threadInfos,
+ useResolvedThreadInfosObjOptions,
+ );
const { chatMentionCandidatesObj, communityThreadIDForGenesisThreads } =
React.useMemo(
() => getChatMentionCandidates(resolvedThreadInfos),
[resolvedThreadInfos],
);
return {
chatMentionCandidatesObj,
resolvedThreadInfos,
communityThreadIDForGenesisThreads,
};
}
function useChatMentionSearchIndex(
chatMentionCandidatesObj: ChatMentionCandidatesObj,
): {
+[id: string]: SentencePrefixSearchIndex,
} {
return React.useMemo(() => {
const result = {};
for (const communityThreadID in chatMentionCandidatesObj) {
const searchIndex = new SentencePrefixSearchIndex();
const searchIndexEntries = [];
for (const threadID in chatMentionCandidatesObj[communityThreadID]) {
searchIndexEntries.push({
id: threadID,
uiName: chatMentionCandidatesObj[communityThreadID][threadID].uiName,
});
}
// Sort the keys so that the order of the search result is consistent
searchIndexEntries.sort(({ uiName: uiNameA }, { uiName: uiNameB }) =>
uiNameA.localeCompare(uiNameB),
);
for (const { id, uiName } of searchIndexEntries) {
searchIndex.addEntry(id, uiName);
}
result[communityThreadID] = searchIndex;
}
return result;
}, [chatMentionCandidatesObj]);
}
export { ChatMentionContextProvider, ChatMentionContext };
diff --git a/lib/utils/entity-helpers.js b/lib/utils/entity-helpers.js
index c578da5cf..4fa733ced 100644
--- a/lib/utils/entity-helpers.js
+++ b/lib/utils/entity-helpers.js
@@ -1,122 +1,128 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import {
ET,
useENSNamesForEntityText,
entityTextToRawString,
} from './entity-text.js';
+import type { UseENSNamesOptions } from '../hooks/ens-cache.js';
import type { ThreadInfo, ResolvedThreadInfo } from '../types/thread-types.js';
import { values } from '../utils/objects.js';
function useResolvedThreadInfos(
threadInfos: $ReadOnlyArray,
+ options?: ?UseENSNamesOptions,
): $ReadOnlyArray {
const entityText = React.useMemo(
() => threadInfos.map(threadInfo => threadInfo.uiName),
[threadInfos],
);
- const withENSNames = useENSNamesForEntityText(entityText);
+ const withENSNames = useENSNamesForEntityText(entityText, options);
invariant(
withENSNames,
'useENSNamesForEntityText only returns falsey when passed falsey',
);
return React.useMemo(
() =>
threadInfos.map((threadInfo, i) => {
if (typeof threadInfo.uiName === 'string') {
// Flow wants return { ...threadInfo, uiName: threadInfo.uiName }
// but that's wasteful and unneeded, so we any-cast here
return (threadInfo: any);
}
const resolvedThreadEntity = withENSNames[i];
return {
...threadInfo,
uiName: entityTextToRawString([resolvedThreadEntity]),
};
}),
[threadInfos, withENSNames],
);
}
function useResolvedOptionalThreadInfos(
threadInfos: ?$ReadOnlyArray,
): ?$ReadOnlyArray {
const entityText = React.useMemo(() => {
if (!threadInfos) {
return null;
}
return threadInfos.map(threadInfo =>
ET.thread({ display: 'uiName', threadInfo }),
);
}, [threadInfos]);
const withENSNames = useENSNamesForEntityText(entityText);
return React.useMemo(() => {
if (!threadInfos) {
return threadInfos;
}
invariant(
withENSNames,
'useENSNamesForEntityText only returns falsey when passed falsey',
);
return threadInfos.map((threadInfo, i) => {
if (typeof threadInfo.uiName === 'string') {
// Flow wants return { ...threadInfo, uiName: threadInfo.uiName }
// but that's wasteful and unneeded, so we any-cast here
return (threadInfo: any);
}
const resolvedThreadEntity = withENSNames[i];
return {
...threadInfo,
uiName: entityTextToRawString([resolvedThreadEntity]),
};
});
}, [threadInfos, withENSNames]);
}
-function useResolvedThreadInfosObj(threadInfosObj: {
- +[id: string]: ThreadInfo,
-}): { +[id: string]: ResolvedThreadInfo } {
+function useResolvedThreadInfosObj(
+ threadInfosObj: { +[id: string]: ThreadInfo },
+ options?: ?UseENSNamesOptions,
+): { +[id: string]: ResolvedThreadInfo } {
const threadInfosArray = React.useMemo(
() => values(threadInfosObj),
[threadInfosObj],
);
- const resolvedThreadInfosArray = useResolvedThreadInfos(threadInfosArray);
+ const resolvedThreadInfosArray = useResolvedThreadInfos(
+ threadInfosArray,
+ options,
+ );
return React.useMemo(() => {
const obj = {};
for (const resolvedThreadInfo of resolvedThreadInfosArray) {
obj[resolvedThreadInfo.id] = resolvedThreadInfo;
}
return obj;
}, [resolvedThreadInfosArray]);
}
function useResolvedThreadInfo(threadInfo: ThreadInfo): ResolvedThreadInfo {
const resolutionInput = React.useMemo(() => [threadInfo], [threadInfo]);
const [resolvedThreadInfo] = useResolvedThreadInfos(resolutionInput);
return resolvedThreadInfo;
}
function useResolvedOptionalThreadInfo(
threadInfo: ?ThreadInfo,
): ?ResolvedThreadInfo {
const resolutionInput = React.useMemo(
() => (threadInfo ? [threadInfo] : []),
[threadInfo],
);
const [resolvedThreadInfo] = useResolvedThreadInfos(resolutionInput);
if (!threadInfo) {
return threadInfo;
}
return resolvedThreadInfo;
}
export {
useResolvedThreadInfos,
useResolvedOptionalThreadInfos,
useResolvedThreadInfosObj,
useResolvedThreadInfo,
useResolvedOptionalThreadInfo,
};
diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js
index d56a0d1f0..e2f6d58da 100644
--- a/lib/utils/entity-text.js
+++ b/lib/utils/entity-text.js
@@ -1,589 +1,592 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import t, { type TInterface, type TUnion } from 'tcomb';
import type { GetENSNames } from './ens-helpers.js';
import { tID, tShape, tString } from './validation-utils.js';
-import { useENSNames } from '../hooks/ens-cache.js';
+import { useENSNames, type UseENSNamesOptions } from '../hooks/ens-cache.js';
import { threadNoun } from '../shared/thread-utils.js';
import { stringForUser } from '../shared/user-utils.js';
import {
type ThreadType,
threadTypes,
threadTypeValidator,
} from '../types/thread-types-enum.js';
import { type RawThreadInfo, type ThreadInfo } from '../types/thread-types.js';
import { basePluralize } from '../utils/text-utils.js';
type UserEntity = {
+type: 'user',
+id: string,
+username?: ?string,
+isViewer?: ?boolean,
+possessive?: ?boolean, // eg. `user's` instead of `user`
};
export const userEntityValidator: TInterface = tShape({
type: tString('user'),
id: t.String,
username: t.maybe(t.String),
isViewer: t.maybe(t.Boolean),
possessive: t.maybe(t.Boolean),
});
// Comments explain how thread name will appear from user4's perspective
export type ThreadEntity =
| {
+type: 'thread',
+id: string,
+name?: ?string,
// displays threadInfo.name if set, or 'user1, user2, and user3'
+display: 'uiName',
// If uiName is EntityText, then at render time ThreadEntity will be
// replaced with a pluralized list of uiName's UserEntities
+uiName: $ReadOnlyArray | string,
// If name isn't set and uiName is an array with only the viewer, then
// just_you_string displays "just you" but viewer_username displays the
// viewer's ENS-resolved username. Defaults to just_you_string
+ifJustViewer?: 'just_you_string' | 'viewer_username',
}
| {
+type: 'thread',
+id: string,
+name?: ?string,
// displays threadInfo.name if set, or eg. 'this thread' or 'this chat'
+display: 'shortName',
+threadType?: ?ThreadType,
+parentThreadID?: ?string,
+alwaysDisplayShortName?: ?boolean, // don't default to name
+subchannel?: ?boolean, // short name should be "subchannel"
+possessive?: ?boolean, // eg. `this thread's` instead of `this thread`
};
export const threadEntityValidator: TUnion = t.union([
tShape({
type: tString('thread'),
id: tID,
name: t.maybe(t.String),
display: tString('uiName'),
uiName: t.union([t.list(userEntityValidator), t.String]),
ifJustViewer: t.maybe(t.enums.of(['just_you_string', 'viewer_username'])),
}),
tShape({
type: tString('thread'),
id: tID,
name: t.maybe(t.String),
display: tString('shortName'),
threadType: t.maybe(threadTypeValidator),
parentThreadID: t.maybe(tID),
alwaysDisplayShortName: t.maybe(t.Boolean),
subchannel: t.maybe(t.Boolean),
possessive: t.maybe(t.Boolean),
}),
]);
type ColorEntity = {
+type: 'color',
+hex: string,
};
type EntityTextComponent = UserEntity | ThreadEntity | ColorEntity | string;
export type EntityText = $ReadOnlyArray;
const entityTextFunction = (
strings: $ReadOnlyArray,
...entities: $ReadOnlyArray
) => {
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,
+subchannel?: ?boolean,
+possessive?: ?boolean,
}
| {
+display: 'alwaysDisplayShortName',
+threadInfo: ThreadInfo | RawThreadInfo,
+possessive?: ?boolean,
}
| {
+display: 'alwaysDisplayShortName',
+threadID: string,
+parentThreadID?: ?string,
+threadType?: ?ThreadType,
+possessive?: ?boolean,
};
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
entityTextFunction.thread = (input: EntityTextThreadInput) => {
if (input.display === 'uiName') {
const { threadInfo } = input;
if (typeof threadInfo.uiName !== 'string') {
return threadInfo.uiName;
}
return {
type: 'thread',
id: threadInfo.id,
name: threadInfo.name,
display: 'uiName',
uiName: threadInfo.uiName,
};
}
if (input.display === 'alwaysDisplayShortName' && input.threadID) {
const { threadID, threadType, parentThreadID, possessive } = input;
return {
type: 'thread',
id: threadID,
name: undefined,
display: 'shortName',
threadType,
parentThreadID,
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,
parentThreadID: threadInfo.parentThreadID,
alwaysDisplayShortName: true,
possessive,
};
} else if (input.display === 'shortName' || !input.display) {
const { threadInfo, subchannel, possessive } = input;
return {
type: 'thread',
id: threadInfo.id,
name: threadInfo.name,
display: 'shortName',
threadType: threadInfo.type,
parentThreadID: threadInfo.parentThreadID,
subchannel,
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,
...entities: $ReadOnlyArray
) => 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,
params?: ?EntityTextToRawStringParams,
): string {
const { name: userGeneratedName, display } = entity;
if (entity.display === 'uiName') {
if (userGeneratedName) {
return userGeneratedName;
}
const { uiName } = entity;
if (typeof uiName === 'string') {
return uiName;
}
let userEntities = uiName;
if (!params?.ignoreViewer) {
const viewerFilteredUserEntities = userEntities.filter(
innerEntity => !innerEntity.isViewer,
);
if (viewerFilteredUserEntities.length > 0) {
userEntities = viewerFilteredUserEntities;
} else if (entity.ifJustViewer === 'viewer_username') {
// We pass ignoreViewer to entityTextToRawString in order
// to prevent it from rendering the viewer as "you"
params = { ...params, ignoreViewer: true };
} else {
return 'just you';
}
}
const pluralized = pluralizeEntityText(
userEntities.map(innerEntity => [innerEntity]),
);
return entityTextToRawString(pluralized, params);
}
invariant(
entity.display === 'shortName',
`getNameForThreadEntity can't handle thread entity display ${display}`,
);
let { name } = entity;
if (!name || entity.alwaysDisplayShortName) {
const threadType = entity.threadType ?? threadTypes.PERSONAL;
const { parentThreadID } = entity;
const noun = entity.subchannel
? 'subchannel'
: threadNoun(threadType, parentThreadID);
if (entity.id === params?.threadID) {
const prefixThisThreadNounWith =
params?.prefixThisThreadNounWith === 'your' ? 'your' : 'this';
name = `${prefixThisThreadNounWith} ${noun}`;
} else {
name = `a ${noun}`;
}
}
if (entity.possessive) {
name = makePossessive({ str: name });
}
return name;
}
function getNameForUserEntity(
entity: UserEntity,
ignoreViewer: ?boolean,
): string {
const isViewer = entity.isViewer && !ignoreViewer;
const entityWithIsViewerIgnored = { ...entity, isViewer };
const str = stringForUser(entityWithIsViewerIgnored);
if (!entityWithIsViewerIgnored.possessive) {
return str;
}
return makePossessive({ str, isViewer });
}
type EntityTextToRawStringParams = {
+threadID?: ?string,
+ignoreViewer?: ?boolean,
+prefixThisThreadNounWith?: ?('this' | 'your'),
};
function entityTextToRawString(
entityText: EntityText,
params?: ?EntityTextToRawStringParams,
): string {
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
const textParts = entityText.map(entity => {
if (typeof entity === 'string') {
return entity;
} else if (entity.type === 'thread') {
return getNameForThreadEntity(entity, params);
} else if (entity.type === 'color') {
return entity.hex;
} else if (entity.type === 'user') {
return getNameForUserEntity(entity, params?.ignoreViewer);
} 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,
+renderUser: ({ +userID: string, +usernameText: string }) => React.Node,
+renderColor: ({ +hex: string }) => React.Node,
};
function entityTextToReact(
entityText: EntityText,
threadID: string,
renderFuncs: RenderFunctions,
): React.Node {
const { renderText, renderThread, renderUser, renderColor } = renderFuncs;
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
return entityText.map((entity, i) => {
const key = `text${i}`;
if (typeof entity === 'string') {
return (
{renderText({ text: entity })}
);
} else if (entity.type === 'thread') {
const { id } = entity;
const name = getNameForThreadEntity(entity, { threadID });
if (id === threadID) {
return name;
} else {
return (
{renderThread({ id, name })}
);
}
} else if (entity.type === 'color') {
return (
{renderColor({ hex: entity.hex })}
);
} else if (entity.type === 'user') {
const userID = entity.id;
const usernameText = getNameForUserEntity(entity);
return (
{renderUser({ userID, usernameText })}
);
} else {
invariant(
false,
`entityTextToReact can't handle entity type ${entity.type}`,
);
}
});
}
function pluralizeEntityText(
nouns: $ReadOnlyArray,
maxNumberOfNouns: number = 3,
): EntityText {
return basePluralize(
nouns,
maxNumberOfNouns,
(a: EntityText | string, b: ?EntityText | string) =>
b ? ET`${a}${b}` : ET`${a}`,
);
}
type TextEntity = { +type: 'text', +text: string };
type ShadowUserEntity = {
+type: 'shadowUser',
+username: string,
+originalUsername: string,
};
type EntityTextComponentAsObject =
| UserEntity
| ThreadEntity
| ColorEntity
| TextEntity
| ShadowUserEntity;
function entityTextToObjects(
entityText: EntityText,
): EntityTextComponentAsObject[] {
const objs = [];
for (const entity of entityText) {
if (typeof entity === 'string') {
objs.push({ type: 'text', text: entity });
continue;
}
objs.push(entity);
if (
entity.type === 'thread' &&
entity.display === 'uiName' &&
typeof entity.uiName !== 'string'
) {
for (const innerEntity of entity.uiName) {
if (typeof innerEntity === 'string' || innerEntity.type !== 'user') {
continue;
}
const { username } = innerEntity;
if (username) {
objs.push({
type: 'shadowUser',
originalUsername: username,
username,
});
}
}
}
}
return objs;
}
function entityTextFromObjects(
objects: $ReadOnlyArray,
): EntityText {
const shadowUserMap = new Map();
for (const obj of objects) {
if (obj.type === 'shadowUser' && obj.username !== obj.originalUsername) {
shadowUserMap.set(obj.originalUsername, obj.username);
}
}
return objects
.map(entity => {
if (entity.type === 'text') {
return entity.text;
} else if (entity.type === 'shadowUser') {
return null;
} else if (
entity.type === 'thread' &&
entity.display === 'uiName' &&
typeof entity.uiName !== 'string'
) {
const uiName = [];
let changeOccurred = false;
for (const innerEntity of entity.uiName) {
if (typeof innerEntity === 'string' || innerEntity.type !== 'user') {
uiName.push(innerEntity);
continue;
}
const { username } = innerEntity;
if (!username) {
uiName.push(innerEntity);
continue;
}
const ensName = shadowUserMap.get(username);
if (!ensName) {
uiName.push(innerEntity);
continue;
}
changeOccurred = true;
uiName.push({
...innerEntity,
username: ensName,
});
}
if (!changeOccurred) {
return entity;
}
return {
...entity,
uiName,
};
} else {
return entity;
}
})
.filter(Boolean);
}
-function useENSNamesForEntityText(entityText: ?EntityText): ?EntityText {
+function useENSNamesForEntityText(
+ entityText: ?EntityText,
+ options?: ?UseENSNamesOptions,
+): ?EntityText {
const allObjects = React.useMemo(
() => (entityText ? entityTextToObjects(entityText) : []),
[entityText],
);
- const objectsWithENSNames = useENSNames(allObjects);
+ const objectsWithENSNames = useENSNames(allObjects, options);
return React.useMemo(
() =>
entityText ? entityTextFromObjects(objectsWithENSNames) : entityText,
[entityText, objectsWithENSNames],
);
}
function useEntityTextAsString(
entityText: ?EntityText,
params?: EntityTextToRawStringParams,
): ?string {
const withENSNames = useENSNamesForEntityText(entityText);
return React.useMemo(() => {
if (!withENSNames) {
return withENSNames;
}
return entityTextToRawString(withENSNames, params);
}, [withENSNames, params]);
}
async function getEntityTextAsString(
entityText: ?EntityText,
getENSNames: ?GetENSNames,
params?: EntityTextToRawStringParams,
): Promise {
if (!entityText) {
return entityText;
}
let resolvedEntityText = entityText;
if (getENSNames) {
const allObjects = entityTextToObjects(entityText);
const objectsWithENSNames = await getENSNames(allObjects);
resolvedEntityText = entityTextFromObjects(objectsWithENSNames);
}
return entityTextToRawString(resolvedEntityText, params);
}
export {
ET,
entityTextToRawString,
entityTextToReact,
pluralizeEntityText,
useENSNamesForEntityText,
useEntityTextAsString,
getEntityTextAsString,
};