Move to react for newlines, emoji, and links in message body
							parent
							
								
									721935b0c8
								
							
						
					
					
						commit
						4e5c8965ff
					
				@ -0,0 +1,40 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  text: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AddNewLines extends React.Component<Props, {}> {
 | 
			
		||||
  public render() {
 | 
			
		||||
    const { text } = this.props;
 | 
			
		||||
    const results: Array<any> = [];
 | 
			
		||||
    const FIND_NEWLINES = /\n/g;
 | 
			
		||||
 | 
			
		||||
    let match = FIND_NEWLINES.exec(text);
 | 
			
		||||
    let last = 0;
 | 
			
		||||
    let count = 1;
 | 
			
		||||
 | 
			
		||||
    if (!match) {
 | 
			
		||||
      return <span>{text}</span>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (match) {
 | 
			
		||||
      if (last < match.index) {
 | 
			
		||||
        const textWithNoNewline = text.slice(last, match.index);
 | 
			
		||||
        results.push(<span key={count++}>{textWithNoNewline}</span>);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      results.push(<br key={count++} />);
 | 
			
		||||
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      last = FIND_NEWLINES.lastIndex;
 | 
			
		||||
      match = FIND_NEWLINES.exec(text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (last < text.length) {
 | 
			
		||||
      results.push(<span key={count++}>{text.slice(last)}</span>);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <span>{results}</span>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,172 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import is from '@sindresorhus/is';
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import EmojiConvertor from 'emoji-js';
 | 
			
		||||
 | 
			
		||||
import { AddNewLines } from './AddNewLines';
 | 
			
		||||
 | 
			
		||||
function getCountOfAllMatches(str: string, regex: RegExp) {
 | 
			
		||||
  let match = regex.exec(str);
 | 
			
		||||
  let count = 0;
 | 
			
		||||
 | 
			
		||||
  if (!regex.global) {
 | 
			
		||||
    return match ? 1 : 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  while (match) {
 | 
			
		||||
    count += 1;
 | 
			
		||||
    match = regex.exec(str);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return count;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function hasNormalCharacters(str: string) {
 | 
			
		||||
  const noEmoji = str.replace(instance.rx_unified, '').trim();
 | 
			
		||||
  return noEmoji.length > 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSizeClass(str: string) {
 | 
			
		||||
  if (hasNormalCharacters(str)) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const emojiCount = getCountOfAllMatches(str, instance.rx_unified);
 | 
			
		||||
  if (emojiCount > 8) {
 | 
			
		||||
    return '';
 | 
			
		||||
  } else if (emojiCount > 6) {
 | 
			
		||||
    return 'small';
 | 
			
		||||
  } else if (emojiCount > 4) {
 | 
			
		||||
    return 'medium';
 | 
			
		||||
  } else if (emojiCount > 2) {
 | 
			
		||||
    return 'large';
 | 
			
		||||
  } else {
 | 
			
		||||
    return 'jumbo';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Taken from emoji-js/replace_unified
 | 
			
		||||
function getEmojiReplacementData(
 | 
			
		||||
  m: string,
 | 
			
		||||
  p1: string | undefined,
 | 
			
		||||
  p2: string | undefined
 | 
			
		||||
) {
 | 
			
		||||
  let val = instance.map.unified[p1];
 | 
			
		||||
  if (val) {
 | 
			
		||||
    let idx = null;
 | 
			
		||||
    if (p2 === '\uD83C\uDFFB') {
 | 
			
		||||
      idx = '1f3fb';
 | 
			
		||||
    }
 | 
			
		||||
    if (p2 === '\uD83C\uDFFC') {
 | 
			
		||||
      idx = '1f3fc';
 | 
			
		||||
    }
 | 
			
		||||
    if (p2 === '\uD83C\uDFFD') {
 | 
			
		||||
      idx = '1f3fd';
 | 
			
		||||
    }
 | 
			
		||||
    if (p2 === '\uD83C\uDFFE') {
 | 
			
		||||
      idx = '1f3fe';
 | 
			
		||||
    }
 | 
			
		||||
    if (p2 === '\uD83C\uDFFF') {
 | 
			
		||||
      idx = '1f3ff';
 | 
			
		||||
    }
 | 
			
		||||
    if (idx) {
 | 
			
		||||
      return {
 | 
			
		||||
        idx,
 | 
			
		||||
        actual: p2,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      idx: val,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  val = instance.map.unified_vars[p1];
 | 
			
		||||
  if (val) {
 | 
			
		||||
    return {
 | 
			
		||||
      idx: val[1],
 | 
			
		||||
      actual: '',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return m;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Some of this logic taken from emoji-js/replacement
 | 
			
		||||
function getImageTag({
 | 
			
		||||
  match,
 | 
			
		||||
  sizeClass,
 | 
			
		||||
  key,
 | 
			
		||||
}: {
 | 
			
		||||
  match: any;
 | 
			
		||||
  sizeClass: string | undefined;
 | 
			
		||||
  key: string | number;
 | 
			
		||||
}) {
 | 
			
		||||
  const result = getEmojiReplacementData(match[0], match[1], match[2]);
 | 
			
		||||
 | 
			
		||||
  if (is.string(result)) {
 | 
			
		||||
    return <span key={key}>{match[0]}</span>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const img = instance.find_image(result.idx);
 | 
			
		||||
  const title = instance.data[result.idx][3][0];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <img
 | 
			
		||||
      key={key}
 | 
			
		||||
      src={img.path}
 | 
			
		||||
      className={classnames('emoji', sizeClass)}
 | 
			
		||||
      data-codepoints={img.full_idx}
 | 
			
		||||
      title={`:${title}:`}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const instance = new EmojiConvertor();
 | 
			
		||||
instance.init_unified();
 | 
			
		||||
instance.init_colons();
 | 
			
		||||
instance.img_sets.apple.path =
 | 
			
		||||
  'node_modules/emoji-datasource-apple/img/apple/64/';
 | 
			
		||||
instance.include_title = true;
 | 
			
		||||
instance.replace_mode = 'img';
 | 
			
		||||
instance.supports_css = false; // needed to avoid spans with background-image
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  text: string;
 | 
			
		||||
  sizeClass?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Emojify extends React.Component<Props, {}> {
 | 
			
		||||
  public render() {
 | 
			
		||||
    const { text, sizeClass } = this.props;
 | 
			
		||||
    const results: Array<any> = [];
 | 
			
		||||
 | 
			
		||||
    let match = instance.rx_unified.exec(text);
 | 
			
		||||
    let last = 0;
 | 
			
		||||
    let count = 1;
 | 
			
		||||
 | 
			
		||||
    if (!match) {
 | 
			
		||||
      return <AddNewLines text={text} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (match) {
 | 
			
		||||
      if (last < match.index) {
 | 
			
		||||
        const textWithNoEmoji = text.slice(last, match.index);
 | 
			
		||||
        results.push(<AddNewLines key={count++} text={textWithNoEmoji} />);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      results.push(getImageTag({ match, sizeClass, key: count++ }));
 | 
			
		||||
 | 
			
		||||
      last = instance.rx_unified.lastIndex;
 | 
			
		||||
      match = instance.rx_unified.exec(text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (last < text.length) {
 | 
			
		||||
      results.push(<AddNewLines key={count++} text={text.slice(last)} />);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <span>{results}</span>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,59 @@
 | 
			
		||||
### Plain text
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="Plain text message" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="Plain text message\n\nWith a new line." />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Jumbo emoji
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="🔥" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="🔥🔥" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="🔥🔥🔥🔥" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Text and emoji
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="Plain text 🔥message. With 🔥emoji🔥 sprinkled 🔥about" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="🔥Message starting and ending with emoji🔥" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Links
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="This before and after link. Before. https://somewhere.com After." />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="Link https://somewhere.com\nWhat do you think? How about this one? \n\nhttps://anotherlink.com" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="Link https://somewhere.com\nWhat do you think? How about this one? \n\nhttps://anotherlink.com" />
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```jsx
 | 
			
		||||
<MessageBody text="should not render as link:\nmailto:someone@somewhere.com\nftp://something.com\n//local/share\n\\local\share\n\nshould render as link:\ngithub.com\nhttps://blah.com" />
 | 
			
		||||
```
 | 
			
		||||
@ -0,0 +1,66 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import createLinkify from 'linkify-it';
 | 
			
		||||
 | 
			
		||||
import { Emojify, getSizeClass } from './Emojify';
 | 
			
		||||
 | 
			
		||||
const linkify = createLinkify();
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  text: string;
 | 
			
		||||
  disableJumbomoji?: boolean;
 | 
			
		||||
  disableLinks?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SUPPORTED_PROTOCOLS = /^(http|https):/i;
 | 
			
		||||
 | 
			
		||||
export class MessageBody extends React.Component<Props, {}> {
 | 
			
		||||
  public render() {
 | 
			
		||||
    const { text, disableJumbomoji, disableLinks } = this.props;
 | 
			
		||||
    const matchData = linkify.match(text) || [];
 | 
			
		||||
    const results: Array<any> = [];
 | 
			
		||||
    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 <Emojify text={text} sizeClass={sizeClass} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    matchData.forEach(
 | 
			
		||||
      (match: {
 | 
			
		||||
        index: number;
 | 
			
		||||
        url: string;
 | 
			
		||||
        lastIndex: number;
 | 
			
		||||
        text: string;
 | 
			
		||||
      }) => {
 | 
			
		||||
        if (last < match.index) {
 | 
			
		||||
          const textWithNoLink = text.slice(last, match.index);
 | 
			
		||||
          results.push(<Emojify key={count++} text={textWithNoLink} />);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { url, text: originalText } = match;
 | 
			
		||||
        if (SUPPORTED_PROTOCOLS.test(url)) {
 | 
			
		||||
          results.push(
 | 
			
		||||
            <a key={count++} href={url}>
 | 
			
		||||
              {originalText}
 | 
			
		||||
            </a>
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
          results.push(<Emojify key={count++} text={originalText} />);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        last = match.lastIndex;
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (last < text.length) {
 | 
			
		||||
      results.push(<Emojify key={count++} text={text.slice(last)} />);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return <span>{results}</span>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue