Move to react for newlines, emoji, and links in message body
This commit is contained in:
parent
721935b0c8
commit
4e5c8965ff
15 changed files with 400 additions and 29 deletions
|
@ -8,14 +8,15 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'none';
|
content="default-src 'none';
|
||||||
|
child-src 'self';
|
||||||
connect-src 'self' https: wss:;
|
connect-src 'self' https: wss:;
|
||||||
|
font-src 'self';
|
||||||
|
frame-src 'none';
|
||||||
|
img-src 'self' blob: data:;
|
||||||
|
media-src 'self' blob:;
|
||||||
|
object-src 'none'"
|
||||||
script-src 'self';
|
script-src 'self';
|
||||||
style-src 'self' 'unsafe-inline';
|
style-src 'self' 'unsafe-inline';
|
||||||
img-src 'self' blob: data:;
|
|
||||||
font-src 'self';
|
|
||||||
media-src 'self' blob:;
|
|
||||||
child-src 'self';
|
|
||||||
object-src 'none'"
|
|
||||||
>
|
>
|
||||||
<title>Signal</title>
|
<title>Signal</title>
|
||||||
<link href='images/icon_128.png' rel='shortcut icon'>
|
<link href='images/icon_128.png' rel='shortcut icon'>
|
||||||
|
@ -283,7 +284,7 @@
|
||||||
{{ #hasBody }}
|
{{ #hasBody }}
|
||||||
<div class='content' dir='auto'>
|
<div class='content' dir='auto'>
|
||||||
{{ #message }}
|
{{ #message }}
|
||||||
<div class='body'>{{ message }}</div>
|
<div class='body'></div>
|
||||||
{{ /message }}
|
{{ /message }}
|
||||||
</div>
|
</div>
|
||||||
{{ /hasBody }}
|
{{ /hasBody }}
|
||||||
|
@ -375,7 +376,7 @@
|
||||||
<span class='unread-count'>{{ unreadCount }}</span>
|
<span class='unread-count'>{{ unreadCount }}</span>
|
||||||
{{ /unreadCount }}
|
{{ /unreadCount }}
|
||||||
{{ #last_message }}
|
{{ #last_message }}
|
||||||
<p class='last-message' dir='auto'> {{ last_message }} </p>
|
<p class='last-message' dir='auto'></p>
|
||||||
{{ /last_message }}
|
{{ /last_message }}
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -23,6 +23,7 @@ const { LightboxGallery } = require('../ts/components/LightboxGallery');
|
||||||
const {
|
const {
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
} = require('../ts/components/conversation/media-gallery/MediaGallery');
|
} = require('../ts/components/conversation/media-gallery/MediaGallery');
|
||||||
|
const { MessageBody } = require('../ts/components/conversation/MessageBody');
|
||||||
const { Quote } = require('../ts/components/conversation/Quote');
|
const { Quote } = require('../ts/components/conversation/Quote');
|
||||||
|
|
||||||
// Migrations
|
// Migrations
|
||||||
|
@ -58,6 +59,7 @@ exports.setup = (options = {}) => {
|
||||||
Lightbox,
|
Lightbox,
|
||||||
LightboxGallery,
|
LightboxGallery,
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
|
MessageBody,
|
||||||
Types: {
|
Types: {
|
||||||
Message: MediaGalleryMessage,
|
Message: MediaGalleryMessage,
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,12 +55,14 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const lastMessage = this.model.get('lastMessage');
|
||||||
|
|
||||||
this.$el.html(
|
this.$el.html(
|
||||||
Mustache.render(
|
Mustache.render(
|
||||||
_.result(this, 'template', ''),
|
_.result(this, 'template', ''),
|
||||||
{
|
{
|
||||||
title: this.model.getTitle(),
|
title: this.model.getTitle(),
|
||||||
last_message: this.model.get('lastMessage'),
|
last_message: Boolean(lastMessage),
|
||||||
last_message_timestamp: this.model.get('timestamp'),
|
last_message_timestamp: this.model.get('timestamp'),
|
||||||
number: this.model.getNumber(),
|
number: this.model.getNumber(),
|
||||||
avatar: this.model.getAvatar(),
|
avatar: this.model.getAvatar(),
|
||||||
|
@ -74,7 +76,23 @@
|
||||||
this.timeStampView.update();
|
this.timeStampView.update();
|
||||||
|
|
||||||
emoji_util.parse(this.$('.name'));
|
emoji_util.parse(this.$('.name'));
|
||||||
emoji_util.parse(this.$('.last-message'));
|
|
||||||
|
if (lastMessage) {
|
||||||
|
if (this.bodyView) {
|
||||||
|
this.bodyView.remove();
|
||||||
|
this.bodyView = null;
|
||||||
|
}
|
||||||
|
this.bodyView = new Whisper.ReactWrapperView({
|
||||||
|
className: 'body-wrapper',
|
||||||
|
Component: window.Signal.Components.MessageBody,
|
||||||
|
props: {
|
||||||
|
text: lastMessage,
|
||||||
|
disableJumbomoji: true,
|
||||||
|
disableLinks: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.$('.last-message').append(this.bodyView.el);
|
||||||
|
}
|
||||||
|
|
||||||
var unread = this.model.get('unreadCount');
|
var unread = this.model.get('unreadCount');
|
||||||
if (unread > 0) {
|
if (unread > 0) {
|
||||||
|
|
|
@ -1293,7 +1293,7 @@
|
||||||
className: 'quote-wrapper',
|
className: 'quote-wrapper',
|
||||||
Component: window.Signal.Components.Quote,
|
Component: window.Signal.Components.Quote,
|
||||||
props: Object.assign({}, props, {
|
props: Object.assign({}, props, {
|
||||||
text: props.text ? window.emoji.signalReplace(props.text) : null,
|
text: props.text,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
this.setQuoteMessage(null);
|
this.setQuoteMessage(null);
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
const URL_REGEX = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[-A-Z0-9\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFD+\u0026\u2019@#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026@#/%=~()_|])/gi;
|
|
||||||
|
|
||||||
const ErrorIconView = Whisper.View.extend({
|
const ErrorIconView = Whisper.View.extend({
|
||||||
templateName: 'error-icon',
|
templateName: 'error-icon',
|
||||||
className: 'error-icon-container',
|
className: 'error-icon-container',
|
||||||
|
@ -440,7 +438,7 @@
|
||||||
className: 'quote-wrapper',
|
className: 'quote-wrapper',
|
||||||
Component: window.Signal.Components.Quote,
|
Component: window.Signal.Components.Quote,
|
||||||
props: Object.assign({}, props, {
|
props: Object.assign({}, props, {
|
||||||
text: props.text ? window.emoji.signalReplace(props.text) : null,
|
text: props.text,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
this.$('.inner-bubble').prepend(this.quoteView.el);
|
this.$('.inner-bubble').prepend(this.quoteView.el);
|
||||||
|
@ -566,11 +564,13 @@
|
||||||
const hasAttachments = attachments && attachments.length > 0;
|
const hasAttachments = attachments && attachments.length > 0;
|
||||||
const hasBody = this.hasTextContents();
|
const hasBody = this.hasTextContents();
|
||||||
|
|
||||||
|
const messageBody = this.model.get('body');
|
||||||
|
|
||||||
this.$el.html(
|
this.$el.html(
|
||||||
Mustache.render(
|
Mustache.render(
|
||||||
_.result(this, 'template', ''),
|
_.result(this, 'template', ''),
|
||||||
{
|
{
|
||||||
message: this.model.get('body'),
|
message: Boolean(messageBody),
|
||||||
hasBody,
|
hasBody,
|
||||||
timestamp: this.model.get('sent_at'),
|
timestamp: this.model.get('sent_at'),
|
||||||
sender: (contact && contact.getTitle()) || '',
|
sender: (contact && contact.getTitle()) || '',
|
||||||
|
@ -589,17 +589,19 @@
|
||||||
|
|
||||||
this.renderControl();
|
this.renderControl();
|
||||||
|
|
||||||
const body = this.$('.body');
|
if (messageBody) {
|
||||||
|
if (this.bodyView) {
|
||||||
emoji_util.parse(body);
|
this.bodyView.remove();
|
||||||
|
this.bodyView = null;
|
||||||
if (body.length > 0) {
|
}
|
||||||
const escapedBody = body.html();
|
this.bodyView = new Whisper.ReactWrapperView({
|
||||||
body.html(
|
className: 'body-wrapper',
|
||||||
escapedBody
|
Component: window.Signal.Components.MessageBody,
|
||||||
.replace(/\n/g, '<br>')
|
props: {
|
||||||
.replace(URL_REGEX, "$1<a href='$2' target='_blank'>$2</a>")
|
text: messageBody,
|
||||||
);
|
},
|
||||||
|
});
|
||||||
|
this.$('.body').append(this.bodyView.el);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderSent();
|
this.renderSent();
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/is": "^0.8.0",
|
"@sindresorhus/is": "^0.8.0",
|
||||||
"@types/google-libphonenumber": "^7.4.14",
|
"@types/google-libphonenumber": "^7.4.14",
|
||||||
|
"@types/linkify-it": "^2.0.3",
|
||||||
"archiver": "^2.1.1",
|
"archiver": "^2.1.1",
|
||||||
"blob-util": "^1.3.0",
|
"blob-util": "^1.3.0",
|
||||||
"blueimp-canvas-to-blob": "^3.14.0",
|
"blueimp-canvas-to-blob": "^3.14.0",
|
||||||
|
|
|
@ -399,6 +399,8 @@ $avatar-size: 44px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
height: 1.2em;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -211,7 +211,7 @@
|
||||||
{{ #hasBody }}
|
{{ #hasBody }}
|
||||||
<div class='content' dir='auto'>
|
<div class='content' dir='auto'>
|
||||||
{{ #message }}
|
{{ #message }}
|
||||||
<div class='body'>{{ message }}</div>
|
<div class='body'></div>
|
||||||
{{ /message }}
|
{{ /message }}
|
||||||
</div>
|
</div>
|
||||||
{{ /hasBody }}
|
{{ /hasBody }}
|
||||||
|
@ -298,7 +298,7 @@
|
||||||
<span class='unread-count'>{{ unreadCount }}</span>
|
<span class='unread-count'>{{ unreadCount }}</span>
|
||||||
{{ /unreadCount }}
|
{{ /unreadCount }}
|
||||||
{{ #last_message }}
|
{{ #last_message }}
|
||||||
<p class='last-message' dir='auto'> {{ last_message }} </p>
|
<p class='last-message' dir='auto'></p>
|
||||||
{{ /last_message }}
|
{{ /last_message }}
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -42,7 +42,7 @@ window.Whisper.View.Templates = {
|
||||||
{{ #hasBody }}
|
{{ #hasBody }}
|
||||||
<div class='content' dir='auto'>
|
<div class='content' dir='auto'>
|
||||||
{{ #message }}
|
{{ #message }}
|
||||||
<div class='body'>{{ message }}</div>
|
<div class='body'></div>
|
||||||
{{ /message }}
|
{{ /message }}
|
||||||
</div>
|
</div>
|
||||||
{{ /hasBody }}
|
{{ /hasBody }}
|
||||||
|
|
40
ts/components/conversation/AddNewLines.tsx
Normal file
40
ts/components/conversation/AddNewLines.tsx
Normal file
|
@ -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>;
|
||||||
|
}
|
||||||
|
}
|
172
ts/components/conversation/Emojify.tsx
Normal file
172
ts/components/conversation/Emojify.tsx
Normal file
|
@ -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>;
|
||||||
|
}
|
||||||
|
}
|
59
ts/components/conversation/MessageBody.md
Normal file
59
ts/components/conversation/MessageBody.md
Normal file
|
@ -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" />
|
||||||
|
```
|
66
ts/components/conversation/MessageBody.tsx
Normal file
66
ts/components/conversation/MessageBody.tsx
Normal file
|
@ -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>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import classnames from 'classnames';
|
||||||
import * as MIME from '../../../ts/types/MIME';
|
import * as MIME from '../../../ts/types/MIME';
|
||||||
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
|
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
|
||||||
|
|
||||||
|
import { MessageBody } from './MessageBody';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachments: Array<QuotedAttachment>;
|
attachments: Array<QuotedAttachment>;
|
||||||
authorColor: string;
|
authorColor: string;
|
||||||
|
@ -111,7 +113,9 @@ export class Quote extends React.Component<Props, {}> {
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
return (
|
return (
|
||||||
<div className="text" dangerouslySetInnerHTML={{ __html: text }} />
|
<div className="text">
|
||||||
|
<MessageBody text={text} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,10 @@
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.1.tgz#55758d44d422756d6329cbf54e6d41931d7ba28f"
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.1.tgz#55758d44d422756d6329cbf54e6d41931d7ba28f"
|
||||||
|
|
||||||
|
"@types/linkify-it@^2.0.3":
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.0.3.tgz#5352a2d7a35d7c77b527483cd6e68da9148bd780"
|
||||||
|
|
||||||
"@types/lodash@^4.14.106":
|
"@types/lodash@^4.14.106":
|
||||||
version "4.14.106"
|
version "4.14.106"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"
|
||||||
|
|
Loading…
Reference in a new issue