Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3504947
D6508.id21845.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
D6508.id21845.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D6508: [lib][native][web] Introduce EntityText
Attached
Detach File
Event Timeline
Log In to Comment