From d9e5338dff39afdd3502a484088f35fb2da7c9f2 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Fri, 18 May 2018 14:48:20 -0700 Subject: [PATCH] Refactor link/emoji/newline components for composability --- ts/components/conversation/AddNewLines.md | 35 ++++++++++ ts/components/conversation/AddNewLines.tsx | 26 +++++-- ts/components/conversation/ContactDetail.tsx | 2 +- ts/components/conversation/Emojify.md | 60 ++++++++++++++++ ts/components/conversation/Emojify.tsx | 28 ++++++-- ts/components/conversation/Linkify.md | 44 ++++++++++++ ts/components/conversation/Linkify.tsx | 72 +++++++++++++++++++ ts/components/conversation/MessageBody.md | 34 ++------- ts/components/conversation/MessageBody.tsx | 73 +++++++------------- ts/styleguide/StyleGuideUtil.ts | 8 --- ts/types/Util.ts | 8 +++ 11 files changed, 295 insertions(+), 95 deletions(-) create mode 100644 ts/components/conversation/AddNewLines.md create mode 100644 ts/components/conversation/Emojify.md create mode 100644 ts/components/conversation/Linkify.md create mode 100644 ts/components/conversation/Linkify.tsx create mode 100644 ts/types/Util.ts diff --git a/ts/components/conversation/AddNewLines.md b/ts/components/conversation/AddNewLines.md new file mode 100644 index 000000000..14d2b35ec --- /dev/null +++ b/ts/components/conversation/AddNewLines.md @@ -0,0 +1,35 @@ +### All newlines + +```jsx + +``` + +### Starting and ending with newlines + +```jsx + +``` + +### With newlines in the middle + +```jsx + +``` + +### No newlines + +```jsx + +``` + +### Providing custom non-newline render function + +```jsx +const renderNonNewLine = ({ text, key }) => ( + This is my custom content! +); +; +``` diff --git a/ts/components/conversation/AddNewLines.tsx b/ts/components/conversation/AddNewLines.tsx index 7e9647b9b..b4bdfe135 100644 --- a/ts/components/conversation/AddNewLines.tsx +++ b/ts/components/conversation/AddNewLines.tsx @@ -1,27 +1,43 @@ import React from 'react'; +import { RenderTextCallback } from '../../types/Util'; + interface Props { text: string; + /** Allows you to customize now non-newlines are rendered. Simplest is just a . */ + renderNonNewLine?: RenderTextCallback; } export class AddNewLines extends React.Component { + public static defaultProps: Partial = { + renderNonNewLine: ({ text, key }) => {text}, + }; + public render() { - const { text } = this.props; + const { text, renderNonNewLine } = this.props; const results: Array = []; const FIND_NEWLINES = /\n/g; + // We have to do this, because renderNonNewLine is not required in our Props object, + // but it is always provided via defaultProps. + if (!renderNonNewLine) { + return; + } + let match = FIND_NEWLINES.exec(text); let last = 0; let count = 1; if (!match) { - return {text}; + return renderNonNewLine({ text, key: 0 }); } while (match) { if (last < match.index) { const textWithNoNewline = text.slice(last, match.index); - results.push({textWithNoNewline}); + results.push( + renderNonNewLine({ text: textWithNoNewline, key: count++ }) + ); } results.push(
); @@ -32,9 +48,9 @@ export class AddNewLines extends React.Component { } if (last < text.length) { - results.push({text.slice(last)}); + results.push(renderNonNewLine({ text: text.slice(last), key: count++ })); } - return {results}; + return results; } } diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx index 4437f6559..1ea63b65a 100644 --- a/ts/components/conversation/ContactDetail.tsx +++ b/ts/components/conversation/ContactDetail.tsx @@ -17,7 +17,7 @@ import { renderSendMessage, } from './EmbeddedContact'; -type Localizer = (key: string, values?: Array) => string; +import { Localizer } from '../../types/Util'; interface Props { contact: Contact; diff --git a/ts/components/conversation/Emojify.md b/ts/components/conversation/Emojify.md new file mode 100644 index 000000000..17e2c9e1a --- /dev/null +++ b/ts/components/conversation/Emojify.md @@ -0,0 +1,60 @@ +### All emoji + +```jsx + +``` + +### With skin color modifier + +```jsx + +``` + +### With `sizeClass` provided + +```jsx + +``` + +```jsx + +``` + +```jsx + +``` + +```jsx + +``` + +```jsx + +``` + +### Starting and ending with emoji + +```jsx + +``` + +### With emoji in the middle + +```jsx + +``` + +### No emoji + +```jsx + +``` + +### Providing custom non-link render function + +```jsx +const renderNonEmoji = ({ text, key }) => ( + This is my custom content +); +; +``` diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx index 6a323d959..591b30522 100644 --- a/ts/components/conversation/Emojify.tsx +++ b/ts/components/conversation/Emojify.tsx @@ -9,7 +9,8 @@ import { getReplacementData, getTitle, } from '../../util/emoji'; -import { AddNewLines } from './AddNewLines'; + +import { RenderTextCallback } from '../../types/Util'; // Some of this logic taken from emoji-js/replacement function getImageTag({ @@ -43,27 +44,40 @@ function getImageTag({ interface Props { text: string; - sizeClass?: string; + /** A class name to be added to the generated emoji images */ + sizeClass?: '' | 'small' | 'medium' | 'large' | 'jumbo'; + /** Allows you to customize now non-newlines are rendered. Simplest is just a . */ + renderNonEmoji?: RenderTextCallback; } export class Emojify extends React.Component { + public static defaultProps: Partial = { + renderNonEmoji: ({ text, key }) => {text}, + }; + public render() { - const { text, sizeClass } = this.props; + const { text, sizeClass, renderNonEmoji } = this.props; const results: Array = []; const regex = getRegex(); + // We have to do this, because renderNonEmoji is not required in our Props object, + // but it is always provided via defaultProps. + if (!renderNonEmoji) { + return; + } + let match = regex.exec(text); let last = 0; let count = 1; if (!match) { - return ; + return renderNonEmoji({ text, key: 0 }); } while (match) { if (last < match.index) { const textWithNoEmoji = text.slice(last, match.index); - results.push(); + results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ })); } results.push(getImageTag({ match, sizeClass, key: count++ })); @@ -73,9 +87,9 @@ export class Emojify extends React.Component { } if (last < text.length) { - results.push(); + results.push(renderNonEmoji({ text: text.slice(last), key: count++ })); } - return {results}; + return results; } } diff --git a/ts/components/conversation/Linkify.md b/ts/components/conversation/Linkify.md new file mode 100644 index 000000000..aa53d40c2 --- /dev/null +++ b/ts/components/conversation/Linkify.md @@ -0,0 +1,44 @@ +### All link + +```jsx + +``` + +### Starting and ending with link + +```jsx + +``` + +### With a link in the middle + +```jsx + +``` + +### No link + +```jsx + +``` + +### Should not render as link + +```jsx + +``` + +### Should render as link + +```jsx + +``` + +### Providing custom non-link render function + +```jsx +const renderNonLink = ({ text, key }) => ( + This is my custom non-link content! +); +; +``` diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx new file mode 100644 index 000000000..bc5217e02 --- /dev/null +++ b/ts/components/conversation/Linkify.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import createLinkify from 'linkify-it'; + +import { RenderTextCallback } from '../../types/Util'; + +const linkify = createLinkify(); + +interface Props { + text: string; + /** Allows you to customize now non-links are rendered. Simplest is just a . */ + renderNonLink?: RenderTextCallback; +} + +const SUPPORTED_PROTOCOLS = /^(http|https):/i; + +export class Linkify extends React.Component { + public static defaultProps: Partial = { + renderNonLink: ({ text, key }) => {text}, + }; + + public render() { + const { text, renderNonLink } = this.props; + const matchData = linkify.match(text) || []; + const results: Array = []; + let last = 0; + let count = 1; + + // We have to do this, because renderNonLink is not required in our Props object, + // but it is always provided via defaultProps. + if (!renderNonLink) { + return; + } + + if (matchData.length === 0) { + return renderNonLink({ text, key: 0 }); + } + + matchData.forEach( + (match: { + index: number; + url: string; + lastIndex: number; + text: string; + }) => { + if (last < match.index) { + const textWithNoLink = text.slice(last, match.index); + results.push(renderNonLink({ text: textWithNoLink, key: count++ })); + } + + const { url, text: originalText } = match; + if (SUPPORTED_PROTOCOLS.test(url)) { + results.push( + + {originalText} + + ); + } else { + results.push(renderNonLink({ text: originalText, key: count++ })); + } + + last = match.lastIndex; + } + ); + + if (last < text.length) { + results.push(renderNonLink({ text: text.slice(last), key: count++ })); + } + + return results; + } +} diff --git a/ts/components/conversation/MessageBody.md b/ts/components/conversation/MessageBody.md index be171b7ce..8c1f36349 100644 --- a/ts/components/conversation/MessageBody.md +++ b/ts/components/conversation/MessageBody.md @@ -1,11 +1,7 @@ -### Plain text +### All components: emoji, links, newline ```jsx - -``` - -```jsx - + ``` ### Jumbo emoji @@ -31,33 +27,17 @@ ``` ```jsx - + ``` -### Text and emoji - -```jsx - -``` +### Jumbomoji disabled ```jsx - + ``` -### Links - -```jsx - -``` - -```jsx - -``` - -```jsx - -``` +### Links disabled ```jsx - + ``` diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index 703d902c9..058c79368 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -1,67 +1,46 @@ import React from 'react'; -import createLinkify from 'linkify-it'; - import { getSizeClass } from '../../util/emoji'; import { Emojify } from './Emojify'; +import { AddNewLines } from './AddNewLines'; +import { Linkify } from './Linkify'; -const linkify = createLinkify(); +import { RenderTextCallback } from '../../types/Util'; interface Props { text: string; + /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */ disableJumbomoji?: boolean; + /** If set, links will be left alone instead of turned into clickable `` tags. */ disableLinks?: boolean; } -const SUPPORTED_PROTOCOLS = /^(http|https):/i; - +const renderNewLines: RenderTextCallback = ({ + text: textWithNewLines, + key, +}) => ; + +const renderLinks: RenderTextCallback = ({ text: textWithLinks, key }) => ( + +); + +/** + * This component makes it very easy to use all three of our message formatting + * components: `Emojify`, `Linkify`, and `AddNewLines`. Because each of them is fully + * configurable with their `renderXXX` props, this component will assemble all three of + * them for you. + */ export class MessageBody extends React.Component { public render() { const { text, disableJumbomoji, disableLinks } = this.props; - const matchData = linkify.match(text) || []; - const results: Array = []; - let last = 0; - let count = 1; - - // We only use this sizeClass if there was no link detected, because jumbo emoji - // only fire when there's no other text in the message. const sizeClass = disableJumbomoji ? '' : getSizeClass(text); - if (disableLinks || matchData.length === 0) { - return ; - } - - matchData.forEach( - (match: { - index: number; - url: string; - lastIndex: number; - text: string; - }) => { - if (last < match.index) { - const textWithNoLink = text.slice(last, match.index); - results.push(); - } - - const { url, text: originalText } = match; - if (SUPPORTED_PROTOCOLS.test(url)) { - results.push( - - {originalText} - - ); - } else { - results.push(); - } - - last = match.lastIndex; - } + return ( + ); - - if (last < text.length) { - results.push(); - } - - return {results}; } } diff --git a/ts/styleguide/StyleGuideUtil.ts b/ts/styleguide/StyleGuideUtil.ts index fdc9d628a..d7ec25285 100644 --- a/ts/styleguide/StyleGuideUtil.ts +++ b/ts/styleguide/StyleGuideUtil.ts @@ -218,11 +218,3 @@ parent.storage.put('regionCode', 'US'); // Telling Lodash to relinquish _ for use by underscore // @ts-ignore _.noConflict(); - -parent.emoji.signalReplace = (html: string): string => { - return html.replace( - /🔥/g, - '' - ); -}; diff --git a/ts/types/Util.ts b/ts/types/Util.ts new file mode 100644 index 000000000..9819b5a85 --- /dev/null +++ b/ts/types/Util.ts @@ -0,0 +1,8 @@ +export type RenderTextCallback = ( + options: { + text: string; + key: number; + } +) => JSX.Element; + +export type Localizer = (key: string, values?: Array) => string;