Skip to content

Commit

Permalink
Refactor link/emoji/newline components for composability
Browse files Browse the repository at this point in the history
  • Loading branch information
scottnonnenberg-signal committed May 23, 2018
1 parent a5416e4 commit d9e5338
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 95 deletions.
35 changes: 35 additions & 0 deletions ts/components/conversation/AddNewLines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### All newlines

```jsx
<AddNewLines text="\n\n\n" />
```

### Starting and ending with newlines

```jsx
<AddNewLines text="\nin between\n" />
```

### With newlines in the middle

```jsx
<AddNewLines text="Before \n\n after" />
```

### No newlines

```jsx
<AddNewLines text="This is the text" />
```

### Providing custom non-newline render function

```jsx
const renderNonNewLine = ({ text, key }) => (
<span key={key}>This is my custom content!</span>
);
<AddNewLines
text="\n first \n second \n"
renderNonNewLine={renderNonNewLine}
/>;
```
26 changes: 21 additions & 5 deletions ts/components/conversation/AddNewLines.tsx
Original file line number Diff line number Diff line change
@@ -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 <span>. */
renderNonNewLine?: RenderTextCallback;
}

export class AddNewLines extends React.Component<Props, {}> {
public static defaultProps: Partial<Props> = {
renderNonNewLine: ({ text, key }) => <span key={key}>{text}</span>,
};

public render() {
const { text } = this.props;
const { text, renderNonNewLine } = this.props;
const results: Array<any> = [];
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 <span>{text}</span>;
return renderNonNewLine({ text, key: 0 });
}

while (match) {
if (last < match.index) {
const textWithNoNewline = text.slice(last, match.index);
results.push(<span key={count++}>{textWithNoNewline}</span>);
results.push(
renderNonNewLine({ text: textWithNoNewline, key: count++ })
);
}

results.push(<br key={count++} />);
Expand All @@ -32,9 +48,9 @@ export class AddNewLines extends React.Component<Props, {}> {
}

if (last < text.length) {
results.push(<span key={count++}>{text.slice(last)}</span>);
results.push(renderNonNewLine({ text: text.slice(last), key: count++ }));
}

return <span>{results}</span>;
return results;
}
}
2 changes: 1 addition & 1 deletion ts/components/conversation/ContactDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
renderSendMessage,
} from './EmbeddedContact';

type Localizer = (key: string, values?: Array<string>) => string;
import { Localizer } from '../../types/Util';

interface Props {
contact: Contact;
Expand Down
60 changes: 60 additions & 0 deletions ts/components/conversation/Emojify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
### All emoji

```jsx
<Emojify text="🔥🔥🔥" />
```

### With skin color modifier

```jsx
<Emojify text="👍🏾" />
```

### With `sizeClass` provided

```jsx
<Emojify text="🔥" sizeClass="jumbo" />
```

```jsx
<Emojify text="🔥" sizeClass="large" />
```

```jsx
<Emojify text="🔥" sizeClass="medium" />
```

```jsx
<Emojify text="🔥" sizeClass="small" />
```

```jsx
<Emojify text="🔥" sizeClass="" />
```

### Starting and ending with emoji

```jsx
<Emojify text="🔥in between🔥" />
```

### With emoji in the middle

```jsx
<Emojify text="Before 🔥🔥 after" />
```

### No emoji

```jsx
<Emojify text="This is the text" />
```

### Providing custom non-link render function

```jsx
const renderNonEmoji = ({ text, key }) => (
<span key={key}>This is my custom content</span>
);
<Emojify text="Before 🔥🔥 after" renderNonEmoji={renderNonEmoji} />;
```
28 changes: 21 additions & 7 deletions ts/components/conversation/Emojify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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 <span>. */
renderNonEmoji?: RenderTextCallback;
}

export class Emojify extends React.Component<Props, {}> {
public static defaultProps: Partial<Props> = {
renderNonEmoji: ({ text, key }) => <span key={key}>{text}</span>,
};

public render() {
const { text, sizeClass } = this.props;
const { text, sizeClass, renderNonEmoji } = this.props;
const results: Array<any> = [];
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 <AddNewLines text={text} />;
return renderNonEmoji({ text, key: 0 });
}

while (match) {
if (last < match.index) {
const textWithNoEmoji = text.slice(last, match.index);
results.push(<AddNewLines key={count++} text={textWithNoEmoji} />);
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ }));
}

results.push(getImageTag({ match, sizeClass, key: count++ }));
Expand All @@ -73,9 +87,9 @@ export class Emojify extends React.Component<Props, {}> {
}

if (last < text.length) {
results.push(<AddNewLines key={count++} text={text.slice(last)} />);
results.push(renderNonEmoji({ text: text.slice(last), key: count++ }));
}

return <span>{results}</span>;
return results;
}
}
44 changes: 44 additions & 0 deletions ts/components/conversation/Linkify.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
### All link

```jsx
<Linkify text="https://somewhere.com" />
```

### Starting and ending with link

```jsx
<Linkify text="https://somewhere.com Yes? No? https://anotherlink.com" />
```

### With a link in the middle

```jsx
<Linkify text="Before. https://somewhere.com After." />
```

### No link

```jsx
<Linkify text="Plain text" />
```

### Should not render as link

```jsx
<Linkify text="smailto:someone@somewhere.com - ftp://something.com - //local/share - \\local\share" />
```

### Should render as link

```jsx
<Linkify text="github.com - https://blah.com" />
```

### Providing custom non-link render function

```jsx
const renderNonLink = ({ text, key }) => (
<span key={key}>This is my custom non-link content!</span>
);
<Linkify text="Before github.com After" renderNonLink={renderNonLink} />;
```
72 changes: 72 additions & 0 deletions ts/components/conversation/Linkify.tsx
Original file line number Diff line number Diff line change
@@ -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 <span>. */
renderNonLink?: RenderTextCallback;
}

const SUPPORTED_PROTOCOLS = /^(http|https):/i;

export class Linkify extends React.Component<Props, {}> {
public static defaultProps: Partial<Props> = {
renderNonLink: ({ text, key }) => <span key={key}>{text}</span>,
};

public render() {
const { text, renderNonLink } = this.props;
const matchData = linkify.match(text) || [];
const results: Array<any> = [];
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(
<a key={count++} href={url}>
{originalText}
</a>
);
} 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;
}
}

0 comments on commit d9e5338

Please sign in to comment.