Twemojified all text

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2021-11-23 11:56:02 +05:30
parent 9d0f99c509
commit 647d085c5f
18 changed files with 266 additions and 248 deletions

View file

@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import './Dialog.scss';
import { twemojify } from '../../../util/twemojify';
import Text from '../../atoms/text/Text';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import ScrollView from '../../atoms/scroll/ScrollView';
@ -22,7 +24,7 @@ function Dialog({
<div className="dialog__content">
<Header>
<TitleWrapper>
<Text variant="h2">{title}</Text>
<Text variant="h2">{twemojify(title)}</Text>
</TitleWrapper>
{contentOptions}
</Header>

View file

@ -3,10 +3,8 @@ import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import './Message.scss';
import linkifyHtml from 'linkifyjs/html';
import parse from 'html-react-parser';
import twemoji from 'twemoji';
import dateFormat from 'dateformat';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
@ -16,6 +14,7 @@ import { redactEvent, sendReaction } from '../../../client/action/roomTimeline';
import {
openEmojiBoard, openProfileViewer, openReadReceipts, replyTo,
} from '../../../client/action/navigation';
import { sanitizeCustomHtml } from '../../../util/sanitize';
import Text from '../../atoms/text/Text';
import RawIcon from '../../atoms/system-icons/RawIcon';
@ -34,8 +33,6 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { sanitizeCustomHtml, sanitizeText } from './sanitize';
function PlaceholderMessage() {
return (
<div className="ph-msg">
@ -61,8 +58,8 @@ function MessageHeader({
return (
<div className="message__header">
<div style={{ color }} className="message__profile">
<Text variant="b1">{parse(twemoji.parse(name))}</Text>
<Text variant="b1">{userId}</Text>
<Text variant="b1">{twemojify(name)}</Text>
<Text variant="b1">{twemojify(userId)}</Text>
</div>
<div className="message__time">
<Text variant="b3">{time}</Text>
@ -82,8 +79,9 @@ function MessageReply({ name, color, body }) {
<div className="message__reply">
<Text variant="b2">
<RawIcon color={color} size="extra-small" src={ReplyArrowIC} />
<span style={{ color }}>{parse(twemoji.parse(name))}</span>
<>{` ${body}`}</>
<span style={{ color }}>{twemojify(name)}</span>
{' '}
{twemojify(body)}
</Text>
</div>
);
@ -105,17 +103,21 @@ function MessageBody({
// if body is not string it is a React element.
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = isCustomHTML ? sanitizeCustomHtml(body) : body;
if (!isCustomHTML) content = sanitizeText(body);
content = linkifyHtml(content, { target: '_blank', rel: 'noreferrer noopener' });
content = twemoji.parse(content);
const content = isCustomHTML
? twemojify(sanitizeCustomHtml(body), undefined, true, false)
: twemojify(body, undefined, true);
const parsed = parse(content);
return (
<div className="message__body">
<div className="text text-b1">
{ msgType === 'm.emote' && `* ${senderName} ` }
{ parsed }
{ msgType === 'm.emote' && (
<>
{'* '}
{twemojify(senderName)}
{' '}
</>
)}
{ content }
</div>
{ isEdited && <Text className="message__body-edited" variant="b3">(edited)</Text>}
</div>
@ -191,7 +193,7 @@ function genReactionMsg(userIds, reaction) {
<>
{msg}
{genLessContText(' reacted with')}
{parse(twemoji.parse(reaction))}
{twemojify(reaction, { className: 'react-emoji' })}
</>
);
}
@ -209,7 +211,7 @@ function MessageReaction({
type="button"
className={`msg__reaction${isActive ? ' msg__reaction--active' : ''}`}
>
{ parse(twemoji.parse(reaction)) }
{ twemojify(reaction, { className: 'react-emoji' }) }
<Text variant="b3" className="msg__reaction-count">{users.length}</Text>
</button>
</Tooltip>

View file

@ -1,14 +1,5 @@
@use '../../atoms/scroll/scrollbar';
.custom-emoji {
height: var(--fs-b1);
margin: 0 !important;
margin-right: 2px !important;
padding: 0 !important;
position: relative;
top: 2px;
}
.message,
.ph-msg {
padding: var(--sp-ultra-tight) var(--sp-normal);
@ -116,10 +107,6 @@
display: flex;
align-items: baseline;
& img.emoji {
@extend .custom-emoji;
}
& .message__profile {
min-width: 0;
color: var(--tc-surface-high);
@ -155,10 +142,6 @@
}
}
.message__reply {
& img.emoji {
@extend .custom-emoji;
height: 14px;
}
.text {
color: var(--tc-surface-low);
white-space: nowrap;
@ -172,18 +155,10 @@
}
.message__body {
word-break: break-word;
& > .text > * {
white-space: pre-wrap;
}
& a {
word-break: break-word;
}
& img.emoji,
& img[data-mx-emoticon] {
@extend .custom-emoji;
}
& span[data-mx-pill] {
background-color: hsla(0, 0%, 64%, 0.15);
padding: 0 2px;
@ -194,6 +169,13 @@
background-color: hsla(0, 0%, 64%, 0.3);
color: var(--tc-surface-high);
}
&[data-mx-ping] {
background-color: var(--bg-ping);
&:hover {
background-color: var(--bg-ping-hover);
}
}
}
& span[data-mx-spoiler] {
@ -257,7 +239,7 @@
border-radius: 4px;
cursor: pointer;
& .emoji {
& .react-emoji {
width: 14px;
height: 14px;
margin: 2px;
@ -266,7 +248,7 @@
margin: 0 var(--sp-ultra-tight);
color: var(--tc-surface-normal)
}
&-tooltip .emoji {
&-tooltip .react-emoji {
width: 14px;
height: 14px;
margin: 0 var(--sp-ultra-tight);

View file

@ -1,153 +0,0 @@
import sanitizeHtml from 'sanitize-html';
import initMatrix from '../../../client/initMatrix';
function sanitizeColorizedTag(tagName, attributes) {
const attribs = { ...attributes };
const styles = [];
if (attributes['data-mx-color']) {
styles.push(`color: ${attributes['data-mx-color']};`);
}
if (attributes['data-mx-bg-color']) {
styles.push(`background-color: ${attributes['data-mx-bg-color']};`);
}
attribs.style = styles.join(' ');
return { tagName, attribs };
}
function sanitizeLinkTag(tagName, attribs) {
const userLink = attribs.href.match(/^https?:\/\/matrix.to\/#\/(@.+:.+)/);
if (userLink !== null) {
// convert user link to pill
const userId = userLink[1];
return {
tagName: 'span',
attribs: {
'data-mx-pill': userId,
},
};
}
return {
tagName,
attribs: {
...attribs,
target: '_blank',
rel: 'noreferrer noopener',
},
};
}
function sanitizeCodeTag(tagName, attributes) {
const attribs = { ...attributes };
let classes = [];
if (attributes.class) {
classes = attributes.class.split(/\s+/).filter((className) => className.match(/^language-(\w+)/));
}
return {
tagName,
attribs: {
...attribs,
class: classes.join(' '),
},
};
}
function sanitizeImgTag(tagName, attributes) {
const mx = initMatrix.matrixClient;
const { src } = attributes;
const attribs = { ...attributes };
delete attribs.src;
if (src.match(/^mxc:\/\//)) {
attribs.src = mx.mxcUrlToHttp(src);
}
return { tagName, attribs };
}
export function sanitizeCustomHtml(body) {
return sanitizeHtml(body, {
allowedTags: [
'font',
'del',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'p',
'a',
'ul',
'ol',
'sup',
'sub',
'li',
'b',
'i',
'u',
'strong',
'em',
'strike',
'code',
'hr',
'br',
'div',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'caption',
'pre',
'span',
'img',
'details',
'summary',
],
allowedClasses: {},
allowedAttributes: {
ol: ['start'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
a: ['name', 'target', 'href', 'rel'],
code: ['class'],
font: ['data-mx-bg-color', 'data-mx-color', 'color', 'style'],
span: ['data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'style', 'data-mx-pill'],
},
allowProtocolRelative: false,
allowedSchemesByTag: {
a: ['https', 'http', 'ftp', 'mailto', 'magnet'],
img: ['https', 'http'],
},
allowedStyles: {
'*': {
color: [/^#(0x)?[0-9a-f]+$/i],
'background-color': [/^#(0x)?[0-9a-f]+$/i],
},
},
nestingLimit: 100,
nonTextTags: [
'style', 'script', 'textarea', 'option', 'mx-reply',
],
transformTags: {
a: sanitizeLinkTag,
img: sanitizeImgTag,
code: sanitizeCodeTag,
font: sanitizeColorizedTag,
span: sanitizeColorizedTag,
},
});
}
export function sanitizeText(body) {
const tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
};
return body.replace(/[&<>]/g, (tag) => tagsToReplace[tag] || tag);
}

View file

@ -2,6 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import './PeopleSelector.scss';
import { twemojify } from '../../../util/twemojify';
import { blurOnBubbling } from '../../atoms/button/script';
import Text from '../../atoms/text/Text';
@ -19,7 +21,7 @@ function PeopleSelector({
type="button"
>
<Avatar imageSrc={avatarSrc} text={name} bgColor={color} size="extra-small" />
<Text className="people-selector__name" variant="b1">{name}</Text>
<Text className="people-selector__name" variant="b1">{twemojify(name)}</Text>
{peopleRole !== null && <Text className="people-selector__role" variant="b3">{peopleRole}</Text>}
</button>
</div>

View file

@ -2,16 +2,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import './RoomIntro.scss';
import Linkify from 'linkifyjs/react';
import { twemojify } from '../../../util/twemojify';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function linkifyContent(content) {
return <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify>;
}
function RoomIntro({
roomId, avatarSrc, name, heading, desc, time,
}) {
@ -19,8 +15,8 @@ function RoomIntro({
<div className="room-intro">
<Avatar imageSrc={avatarSrc} text={name} bgColor={colorMXID(roomId)} size="large" />
<div className="room-intro__content">
<Text className="room-intro__name" variant="h1">{heading}</Text>
<Text className="room-intro__desc" variant="b1">{linkifyContent(desc)}</Text>
<Text className="room-intro__name" variant="h1">{twemojify(heading)}</Text>
<Text className="room-intro__desc" variant="b1">{twemojify(desc, undefined, true)}</Text>
{ time !== null && <Text className="room-intro__time" variant="b3">{time}</Text>}
</div>
</div>

View file

@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import './RoomSelector.scss';
import { twemojify } from '../../../util/twemojify';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
@ -57,7 +58,7 @@ function RoomSelector({
iconSrc={iconSrc}
size="extra-small"
/>
<Text variant="b1">{name}</Text>
<Text variant="b1">{twemojify(name)}</Text>
{ isUnread && (
<NotificationBadge
alert={isAlert}

View file

@ -2,16 +2,14 @@ import React from 'react';
import PropTypes from 'prop-types';
import './RoomTile.scss';
import Linkify from 'linkifyjs/react';
import { twemojify } from '../../../util/twemojify';
import { sanitizeText } from '../../../util/sanitize';
import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import Avatar from '../../atoms/avatar/Avatar';
function linkifyContent(content) {
return <Linkify options={{ target: { url: '_blank' } }}>{content}</Linkify>;
}
function RoomTile({
avatarSrc, name, id,
inviterName, memberCount, desc, options,
@ -26,7 +24,7 @@ function RoomTile({
/>
</div>
<div className="room-tile__content">
<Text variant="s1">{name}</Text>
<Text variant="s1">{twemojify(name)}</Text>
<Text variant="b3">
{
inviterName !== null
@ -36,7 +34,7 @@ function RoomTile({
</Text>
{
desc !== null && (typeof desc === 'string')
? <Text className="room-tile__content__desc" variant="b2">{linkifyContent(desc)}</Text>
? <Text className="room-tile__content__desc" variant="b2">{twemojify(desc, undefined, true)}</Text>
: desc
}
</div>