Page MenuHomePhabricator

D6508.id21845.diff
No OneTemporary

D6508.id21845.diff

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
@@ -46,6 +46,7 @@
RelativeUserInfo,
} from '../types/user-types';
import { threeDays } from '../utils/date-utils';
+import type { EntityText } from '../utils/entity-text';
import memoize2 from '../utils/memoize';
import {
threadInfoSelector,
@@ -275,7 +276,7 @@
+startsConversation: boolean,
+startsCluster: boolean,
endsCluster: boolean,
- +robotext: string,
+ +robotext: EntityText | string,
+threadCreatedFromMessage: ?ThreadInfo,
+reactions: $ReadOnlyMap<string, MessageReactionInfo>,
};
diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js
--- a/lib/shared/message-utils.js
+++ b/lib/shared/message-utils.js
@@ -31,6 +31,7 @@
} from '../types/messages/reaction';
import { type ThreadInfo } from '../types/thread-types';
import type { RelativeUserInfo, UserInfos } from '../types/user-types';
+import { type EntityText, entityTextToRawString } from '../utils/entity-text';
import { codeBlockRegex, type ParserRules } from './markdown';
import { messageSpecs } from './messages/message-specs';
import { threadIsGroupChat } from './thread-utils';
@@ -90,7 +91,7 @@
function robotextForMessageInfo(
messageInfo: RobotextMessageInfo,
threadInfo: ?ThreadInfo,
-): string {
+): string | EntityText {
const creator = robotextForUser(messageInfo.creator);
const messageSpec = messageSpecs[messageInfo.type];
invariant(
@@ -105,7 +106,10 @@
});
}
-function robotextToRawString(robotext: string): string {
+function robotextToRawString(robotext: string | EntityText): string {
+ if (typeof robotext !== 'string') {
+ return entityTextToRawString(robotext);
+ }
return decodeURI(robotext.replace(/<([^<>|]+)\|[^<>|]+>/g, '$1'));
}
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
@@ -14,6 +14,7 @@
import type { NotifTexts } from '../../types/notif-types';
import type { ThreadInfo, ThreadType } from '../../types/thread-types';
import type { RelativeUserInfo } from '../../types/user-types';
+import type { EntityText } from '../../utils/entity-text';
import { type ParserRules } from '../markdown';
import type { GetMessageTitleViewerContext } from '../message-utils';
@@ -100,7 +101,7 @@
messageInfo: Info,
creator: string,
params: RobotextParams,
- ) => string,
+ ) => string | EntityText,
+shimUnsupportedMessageInfo?: (
rawMessageInfo: RawInfo,
platformDetails: ?PlatformDetails,
diff --git a/lib/shared/notif-utils.js b/lib/shared/notif-utils.js
--- a/lib/shared/notif-utils.js
+++ b/lib/shared/notif-utils.js
@@ -96,12 +96,24 @@
threadInfo: ThreadInfo,
): string {
const robotext = robotextForMessageInfo(messageInfo, threadInfo);
- const threadEntityRegex = new RegExp(`<[^<>\\|]+\\|t${threadInfo.id}>`);
- const threadMadeExplicit = robotext.replace(
- threadEntityRegex,
- notifThreadName(threadInfo),
- );
- return robotextToRawString(threadMadeExplicit);
+ const threadName = notifThreadName(threadInfo);
+ if (typeof robotext === 'string') {
+ const threadEntityRegex = new RegExp(`<[^<>\\|]+\\|t${threadInfo.id}>`);
+ const threadMadeExplicit = robotext.replace(threadEntityRegex, threadName);
+ return robotextToRawString(threadMadeExplicit);
+ } else {
+ const threadMadeExplicit = robotext.map(entity => {
+ if (
+ typeof entity !== 'string' &&
+ entity.type === 'thread' &&
+ entity.id === threadInfo.id
+ ) {
+ return threadName;
+ }
+ return entity;
+ });
+ return robotextToRawString(threadMadeExplicit);
+ }
}
function notifCollapseKeyForRawMessageInfo(
diff --git a/lib/utils/entity-text.js b/lib/utils/entity-text.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/entity-text.js
@@ -0,0 +1,194 @@
+// @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';
+
+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(
+ 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;
+}
+
+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 };
diff --git a/lib/utils/entity-text.test.js b/lib/utils/entity-text.test.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/entity-text.test.js
@@ -0,0 +1,25 @@
+// @flow
+
+import { ET } from './entity-text';
+
+const someUserEntity = {
+ type: 'user',
+ id: '123',
+ username: 'hello',
+};
+
+const someThreadEntity = {
+ type: 'thread',
+ id: '456',
+ display: 'uiName',
+ name: undefined,
+ uiName: 'this is a thread',
+};
+
+describe('ET tag function', () => {
+ it('should compose', () => {
+ const first = ET`${someUserEntity} created ${someThreadEntity}`;
+ const second = ET`${someUserEntity} sent a message: ${first}`;
+ expect(second.length).toBe(5);
+ });
+});
diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -4,7 +4,7 @@
import * as React from 'react';
import type { ChatMessageItem } from 'lib/selectors/chat-selectors';
-import { messageID } from 'lib/shared/message-utils';
+import { messageID, robotextToRawString } from 'lib/shared/message-utils';
import { messageTypes, type MessageType } from 'lib/types/message-types';
import NodeHeightMeasurer from '../components/node-height-measurer.react';
@@ -28,8 +28,8 @@
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return messageInfo.text;
- } else if (item.robotext && typeof item.robotext === 'string') {
- return item.robotext;
+ } else if (item.robotext) {
+ return robotextToRawString(item.robotext);
}
return null;
};
@@ -42,7 +42,7 @@
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return dummyNodeForTextMessageHeightMeasurement(messageInfo.text);
- } else if (item.robotext && typeof item.robotext === 'string') {
+ } else if (item.robotext) {
return dummyNodeForRobotextMessageHeightMeasurement(item.robotext);
}
invariant(false, 'NodeHeightMeasurer asked for dummy for non-text message');
diff --git a/native/chat/inner-robotext-message.react.js b/native/chat/inner-robotext-message.react.js
--- a/native/chat/inner-robotext-message.react.js
+++ b/native/chat/inner-robotext-message.react.js
@@ -10,6 +10,7 @@
parseRobotextEntity,
robotextToRawString,
} from 'lib/shared/message-utils';
+import { entityTextToReact, type EntityText } from 'lib/utils/entity-text';
import Markdown from '../markdown/markdown.react';
import { inlineMarkdownRules } from '../markdown/rules.react';
@@ -19,7 +20,7 @@
import { useNavigateToThread } from './message-list-types';
function dummyNodeForRobotextMessageHeightMeasurement(
- robotext: string,
+ robotext: string | EntityText,
): React.Element<typeof View> {
return (
<View style={unboundStyles.robotextContainer}>
@@ -43,6 +44,26 @@
const { messageInfo, robotext } = item;
const { threadID } = messageInfo;
const textParts = React.useMemo(() => {
+ const darkColor = activeTheme === 'dark';
+
+ if (typeof robotext !== 'string') {
+ return entityTextToReact(robotext, threadID, {
+ // eslint-disable-next-line react/display-name
+ renderText: ({ text }) => (
+ <Markdown
+ style={styles.robotext}
+ rules={inlineMarkdownRules(darkColor)}
+ >
+ {text}
+ </Markdown>
+ ),
+ // eslint-disable-next-line react/display-name
+ renderThread: ({ id, name }) => <ThreadEntity id={id} name={name} />,
+ // eslint-disable-next-line react/display-name
+ renderColor: ({ hex }) => <ColorEntity color={hex} />,
+ });
+ }
+
const robotextParts = splitRobotext(robotext);
const result = [];
let keyIndex = 0;
@@ -53,7 +74,6 @@
}
const key = `text${keyIndex++}`;
if (splitPart.charAt(0) !== '<') {
- const darkColor = activeTheme === 'dark';
result.push(
<Markdown
style={styles.robotext}
diff --git a/native/types/chat-types.js b/native/types/chat-types.js
--- a/native/types/chat-types.js
+++ b/native/types/chat-types.js
@@ -8,6 +8,7 @@
} from 'lib/types/message-types';
import type { TextMessageInfo } from 'lib/types/messages/text';
import type { ThreadInfo } from 'lib/types/thread-types';
+import type { EntityText } from 'lib/utils/entity-text';
import type { MessagePendingUploads } from '../input/input-state';
@@ -19,7 +20,7 @@
+startsConversation: boolean,
+startsCluster: boolean,
+endsCluster: boolean,
- +robotext: string,
+ +robotext: string | EntityText,
+threadCreatedFromMessage: ?ThreadInfo,
+contentHeight: number,
+reactions: $ReadOnlyMap<string, MessageReactionInfo>,
diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js
--- a/web/chat/robotext-message.react.js
+++ b/web/chat/robotext-message.react.js
@@ -8,6 +8,7 @@
import { splitRobotext, parseRobotextEntity } from 'lib/shared/message-utils';
import type { Dispatch } from 'lib/types/redux-types';
import { type ThreadInfo } from 'lib/types/thread-types';
+import { type EntityText, entityTextToReact } from 'lib/utils/entity-text';
import Markdown from '../markdown/markdown.react';
import { linkRules } from '../markdown/rules.react';
@@ -70,6 +71,15 @@
linkedRobotext() {
const { item } = this.props;
const { robotext } = item;
+ const { threadID } = item.messageInfo;
+ if (typeof robotext === 'string') {
+ return this.renderStringRobotext(robotext, threadID);
+ } else {
+ return this.renderEntityText(robotext, threadID);
+ }
+ }
+
+ renderStringRobotext(robotext: string, threadID: string): React.Node {
const robotextParts = splitRobotext(robotext);
const textParts = [];
let keyIndex = 0;
@@ -89,7 +99,7 @@
const { rawText, entityType, id } = parseRobotextEntity(splitPart);
- if (entityType === 't' && id !== item.messageInfo.threadID) {
+ if (entityType === 't' && id !== threadID) {
textParts.push(<ThreadEntity key={key} id={id} name={rawText} />);
} else if (entityType === 'c') {
textParts.push(<ColorEntity key={key} color={rawText} />);
@@ -100,6 +110,16 @@
return textParts;
}
+
+ renderEntityText(entityText: EntityText, threadID: string): React.Node {
+ return entityTextToReact(entityText, threadID, {
+ renderText: ({ text }) => (
+ <Markdown rules={linkRules(false)}>{text}</Markdown>
+ ),
+ renderThread: ({ id, name }) => <ThreadEntity id={id} name={name} />,
+ renderColor: ({ hex }) => <ColorEntity color={hex} />,
+ });
+ }
}
type BaseInnerThreadEntityProps = {
+id: string,

File Metadata

Mime Type
text/plain
Expires
Sat, Dec 21, 11:26 AM (20 h, 35 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2681820
Default Alt Text
D6508.id21845.diff (16 KB)

Event Timeline