Better emoji support in linkify/previews

This commit is contained in:
Fedor Indutny 2021-06-30 10:00:02 -07:00 committed by GitHub
parent 65ad608aa7
commit 773aa9af19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 337 additions and 260 deletions

View file

@ -5,9 +5,9 @@ import React from 'react';
import classNames from 'classnames';
import emojiRegex from 'emoji-regex';
import { RenderTextCallbackType } from '../../types/Util';
import { splitByEmoji } from '../../util/emoji';
import { missingCaseError } from '../../util/missingCaseError';
import { emojiToImage, SizeClassType } from '../emoji/lib';
// Some of this logic taken from emoji-js/replacement
@ -19,23 +19,23 @@ function getImageTag({
sizeClass,
key,
}: {
match: RegExpExecArray;
match: string;
sizeClass?: SizeClassType;
key: string | number;
}) {
const img = emojiToImage(match[0]);
}): JSX.Element | string {
const img = emojiToImage(match);
if (!img) {
return match[0];
return match;
}
return (
<img
key={key}
src={img}
aria-label={match[0]}
aria-label={match}
className={classNames('emoji', sizeClass)}
title={match[0]}
title={match}
/>
);
}
@ -53,15 +53,8 @@ export class Emojify extends React.Component<Props> {
renderNonEmoji: ({ text }) => text,
};
public render():
| JSX.Element
| string
| null
| Array<JSX.Element | string | null> {
public render(): null | Array<JSX.Element | string | null> {
const { text, sizeClass, renderNonEmoji } = this.props;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const results: Array<any> = [];
const regex = emojiRegex();
// We have to do this, because renderNonEmoji is not required in our Props object,
// but it is always provided via defaultProps.
@ -69,33 +62,16 @@ export class Emojify extends React.Component<Props> {
return null;
}
let match = regex.exec(text);
let last = 0;
let count = 1;
if (!match) {
return renderNonEmoji({ text, key: 0 });
}
while (match) {
if (last < match.index) {
const textWithNoEmoji = text.slice(last, match.index);
count += 1;
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count }));
return splitByEmoji(text).map(({ type, value: match }, index) => {
if (type === 'emoji') {
return getImageTag({ match, sizeClass, key: index });
}
count += 1;
results.push(getImageTag({ match, sizeClass, key: count }));
if (type === 'text') {
return renderNonEmoji({ text: match, key: index });
}
last = regex.lastIndex;
match = regex.exec(text);
}
if (last < text.length) {
count += 1;
results.push(renderNonEmoji({ text: text.slice(last), key: count }));
}
return results;
throw missingCaseError(type);
});
}
}

View file

@ -32,6 +32,14 @@ story.add('Links with Text', () => {
return <Linkify {...props} />;
});
story.add('Links with Emoji without space', () => {
const props = createProps({
text: '👍https://www.signal.org😎',
});
return <Linkify {...props} />;
});
story.add('No Link', () => {
const props = createProps({
text: 'I am fond of cats',

View file

@ -6,7 +6,9 @@ import React from 'react';
import LinkifyIt from 'linkify-it';
import { RenderTextCallbackType } from '../../types/Util';
import { isLinkSneaky } from '../../../js/modules/link_previews';
import { isLinkSneaky } from '../../types/LinkPreview';
import { splitByEmoji } from '../../util/emoji';
import { missingCaseError } from '../../util/missingCaseError';
const linkify = LinkifyIt()
// This is all of the TLDs in place in 2010, according to [Wikipedia][0]. Note that
@ -55,10 +57,6 @@ export class Linkify extends React.Component<Props> {
| null
| Array<JSX.Element | string | null> {
const { text, renderNonLink } = this.props;
const matchData = linkify.match(text) || [];
const results: Array<JSX.Element | string> = [];
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.
@ -66,19 +64,34 @@ export class Linkify extends React.Component<Props> {
return null;
}
if (matchData.length === 0) {
return renderNonLink({ text, key: 0 });
}
const chunkData: Array<{
chunk: string;
matchData: LinkifyIt.Match[];
}> = splitByEmoji(text).map(({ type, value: chunk }) => {
if (type === 'text') {
return { chunk, matchData: linkify.match(chunk) || [] };
}
matchData.forEach(
(match: {
index: number;
url: string;
lastIndex: number;
text: string;
}) => {
if (type === 'emoji') {
return { chunk, matchData: [] };
}
throw missingCaseError(type);
});
const results: Array<JSX.Element | string> = [];
let last = 0;
let count = 1;
chunkData.forEach(({ chunk, matchData }) => {
if (matchData.length === 0) {
results.push(renderNonLink({ text: chunk, key: 0 }));
return;
}
matchData.forEach(match => {
if (last < match.index) {
const textWithNoLink = text.slice(last, match.index);
const textWithNoLink = chunk.slice(last, match.index);
count += 1;
results.push(renderNonLink({ text: textWithNoLink, key: count }));
}
@ -96,13 +109,13 @@ export class Linkify extends React.Component<Props> {
}
last = match.lastIndex;
}
);
});
if (last < text.length) {
count += 1;
results.push(renderNonLink({ text: text.slice(last), key: count }));
}
if (last < chunk.length) {
count += 1;
results.push(renderNonLink({ text: chunk.slice(last), key: count }));
}
});
return results;
}