Twemojified all text
Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
parent
9d0f99c509
commit
647d085c5f
18 changed files with 266 additions and 248 deletions
|
@ -2,6 +2,8 @@ import React, { useState, useEffect } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import './Avatar.scss';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import Text from '../text/Text';
|
||||
import RawIcon from '../system-icons/RawIcon';
|
||||
|
||||
|
@ -29,7 +31,11 @@ function Avatar({
|
|||
{
|
||||
iconSrc !== null
|
||||
? <RawIcon size={size} src={iconSrc} />
|
||||
: text !== null && <Text variant={textSize}>{[...text][0]}</Text>
|
||||
: text !== null && (
|
||||
<Text variant={textSize}>
|
||||
{twemojify([...text][0])}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
)
|
||||
|
|
|
@ -4,12 +4,26 @@
|
|||
font-weight: $weight;
|
||||
letter-spacing: var(--ls-#{$type});
|
||||
line-height: var(--lh-#{$type});
|
||||
|
||||
& img.emoji,
|
||||
& img[data-mx-emoticon] {
|
||||
height: var(--fs-#{$type});
|
||||
}
|
||||
}
|
||||
|
||||
%text {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--tc-surface-high);
|
||||
|
||||
& img.emoji,
|
||||
& img[data-mx-emoticon] {
|
||||
margin: 0 !important;
|
||||
margin-right: 2px !important;
|
||||
padding: 0 !important;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-h1 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
@ -173,17 +156,9 @@
|
|||
.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);
|
||||
|
|
|
@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
};
|
||||
return body.replace(/[&<>]/g, (tag) => tagsToReplace[tag] || tag);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import './DrawerBreadcrumb.scss';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import { selectSpace } from '../../../client/action/navigation';
|
||||
|
@ -101,7 +103,7 @@ function DrawerBreadcrumb({ spaceId }) {
|
|||
className={index === spacePath.length - 1 ? 'breadcrumb__btn--selected' : ''}
|
||||
onClick={() => selectSpace(id)}
|
||||
>
|
||||
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : mx.getRoom(id).name}</Text>
|
||||
<Text variant="b2">{id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)}</Text>
|
||||
{ noti !== null && (
|
||||
<NotificationBadge
|
||||
alert={noti.highlight !== 0}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import {
|
||||
|
@ -30,7 +32,7 @@ function DrawerHeader({ selectedTab, spaceId }) {
|
|||
return (
|
||||
<Header>
|
||||
<TitleWrapper>
|
||||
<Text variant="s1">{spaceName || tabName}</Text>
|
||||
<Text variant="s1">{twemojify(spaceName) || tabName}</Text>
|
||||
</TitleWrapper>
|
||||
{spaceName && (
|
||||
<IconButton
|
||||
|
|
|
@ -2,6 +2,8 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import './ProfileViewer.scss';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import cons from '../../../client/state/cons';
|
||||
import navigation from '../../../client/state/navigation';
|
||||
|
@ -262,8 +264,8 @@ function ProfileViewer() {
|
|||
size="large"
|
||||
/>
|
||||
<div className="profile-viewer__user__info">
|
||||
<Text variant="s1">{username}</Text>
|
||||
<Text variant="b2">{userId}</Text>
|
||||
<Text variant="s1">{twemojify(username)}</Text>
|
||||
<Text variant="b2">{twemojify(userId)}</Text>
|
||||
</div>
|
||||
<div className="profile-viewer__user__role">
|
||||
<Text variant="b3">Role</Text>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { twemojify } from '../../../util/twemojify';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { openRoomOptions } from '../../../client/action/navigation';
|
||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||
|
@ -27,8 +29,8 @@ function RoomViewHeader({ roomId }) {
|
|||
<Header>
|
||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||
<TitleWrapper>
|
||||
<Text variant="h2">{roomName}</Text>
|
||||
{ typeof roomTopic !== 'undefined' && <p title={roomTopic} className="text text-b3">{roomTopic}</p>}
|
||||
<Text variant="h2">{twemojify(roomName)}</Text>
|
||||
{ typeof roomTopic !== 'undefined' && <p title={roomTopic} className="text text-b3">{twemojify(roomTopic)}</p>}
|
||||
</TitleWrapper>
|
||||
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||
<IconButton
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import { sanitizeText } from '../../../util/sanitize';
|
||||
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
import { getUsername, getUsernameOfRoomMember } from '../../../util/matrixUtil';
|
||||
|
||||
const getEmojifiedJsx = (username) => parse(twemoji.parse(sanitizeText(username)));
|
||||
|
||||
function getTimelineJSXMessages() {
|
||||
return {
|
||||
join(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' joined the room'}
|
||||
</>
|
||||
);
|
||||
|
@ -17,27 +23,27 @@ function getTimelineJSXMessages() {
|
|||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' left the room'}
|
||||
{reasonMsg}
|
||||
{getEmojifiedJsx(reasonMsg)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
invite(inviter, user) {
|
||||
return (
|
||||
<>
|
||||
<b>{inviter}</b>
|
||||
<b>{getEmojifiedJsx(inviter)}</b>
|
||||
{' invited '}
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
cancelInvite(inviter, user) {
|
||||
return (
|
||||
<>
|
||||
<b>{inviter}</b>
|
||||
<b>{getEmojifiedJsx(inviter)}</b>
|
||||
{' canceled '}
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{'\'s invite'}
|
||||
</>
|
||||
);
|
||||
|
@ -45,7 +51,7 @@ function getTimelineJSXMessages() {
|
|||
rejectInvite(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' rejected the invitation'}
|
||||
</>
|
||||
);
|
||||
|
@ -54,10 +60,10 @@ function getTimelineJSXMessages() {
|
|||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
||||
return (
|
||||
<>
|
||||
<b>{actor}</b>
|
||||
<b>{getEmojifiedJsx(actor)}</b>
|
||||
{' kicked '}
|
||||
<b>{user}</b>
|
||||
{reasonMsg}
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{getEmojifiedJsx(reasonMsg)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
@ -65,26 +71,26 @@ function getTimelineJSXMessages() {
|
|||
const reasonMsg = (typeof reason === 'string') ? `: ${reason}` : '';
|
||||
return (
|
||||
<>
|
||||
<b>{actor}</b>
|
||||
<b>{getEmojifiedJsx(actor)}</b>
|
||||
{' banned '}
|
||||
<b>{user}</b>
|
||||
{reasonMsg}
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{getEmojifiedJsx(reasonMsg)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
unban(actor, user) {
|
||||
return (
|
||||
<>
|
||||
<b>{actor}</b>
|
||||
<b>{getEmojifiedJsx(actor)}</b>
|
||||
{' unbanned '}
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
avatarSets(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' set the avatar'}
|
||||
</>
|
||||
);
|
||||
|
@ -92,7 +98,7 @@ function getTimelineJSXMessages() {
|
|||
avatarChanged(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' changed the avatar'}
|
||||
</>
|
||||
);
|
||||
|
@ -100,7 +106,7 @@ function getTimelineJSXMessages() {
|
|||
avatarRemoved(user) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' removed the avatar'}
|
||||
</>
|
||||
);
|
||||
|
@ -108,27 +114,27 @@ function getTimelineJSXMessages() {
|
|||
nameSets(user, newName) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' set the display name to '}
|
||||
<b>{newName}</b>
|
||||
<b>{getEmojifiedJsx(newName)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
nameChanged(user, newName) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' changed the display name to '}
|
||||
<b>{newName}</b>
|
||||
<b>{getEmojifiedJsx(newName)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
nameRemoved(user, lastName) {
|
||||
return (
|
||||
<>
|
||||
<b>{user}</b>
|
||||
<b>{getEmojifiedJsx(user)}</b>
|
||||
{' removed the display name '}
|
||||
<b>{lastName}</b>
|
||||
<b>{getEmojifiedJsx(lastName)}</b>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
@ -141,7 +147,7 @@ function getUsersActionJsx(roomId, userIds, actionStr) {
|
|||
if (room?.getMember(userId)) return getUsernameOfRoomMember(room.getMember(userId));
|
||||
return getUsername(userId);
|
||||
};
|
||||
const getUserJSX = (userId) => <b>{getUserDisplayName(userId)}</b>;
|
||||
const getUserJSX = (userId) => <b>{getEmojifiedJsx(getUserDisplayName(userId))}</b>;
|
||||
if (!Array.isArray(userIds)) return 'Idle';
|
||||
if (userIds.length === 0) return 'Idle';
|
||||
const MAX_VISIBLE_COUNT = 3;
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
|
||||
--bg-tooltip: #353535;
|
||||
--bg-badge: #989898;
|
||||
--bg-ping: hsla(137deg, 100%, 68%, 40%);
|
||||
--bg-ping-hover: hsla(137deg, 100%, 68%, 50%);
|
||||
|
||||
/* text color | --tc-[background type]-[priority]: value */
|
||||
--tc-surface-high: #000000;
|
||||
|
@ -183,6 +185,9 @@
|
|||
|
||||
--bg-tooltip: #000;
|
||||
--bg-badge: hsl(0, 0%, 75%);
|
||||
--bg-ping: hsla(137deg, 100%, 38%, 40%);
|
||||
--bg-ping-hover: hsla(137deg, 100%, 38%, 50%);
|
||||
|
||||
|
||||
/* text color | --tc-[background type]-[priority]: value */
|
||||
--tc-surface-high: rgba(255, 255, 255, 98%);
|
||||
|
|
128
src/util/sanitize.js
Normal file
128
src/util/sanitize.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
import sanitizeHtml from 'sanitize-html';
|
||||
import initMatrix from '../client/initMatrix';
|
||||
|
||||
const MAX_TAG_NESTING = 100;
|
||||
|
||||
const permittedHtmlTags = [
|
||||
'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',
|
||||
];
|
||||
|
||||
const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
const permittedTagToAttributes = {
|
||||
font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'],
|
||||
span: ['style', 'data-mx-bg-color', 'data-mx-color', 'data-mx-spoiler', 'data-mx-pill', 'data-mx-ping'],
|
||||
a: ['name', 'target', 'href', 'rel'],
|
||||
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
|
||||
o: ['start'],
|
||||
code: ['class'],
|
||||
};
|
||||
|
||||
function transformFontTag(tagName, attribs) {
|
||||
return {
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformSpanTag(tagName, attribs) {
|
||||
return {
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformATag(tagName, attribs) {
|
||||
const userLink = attribs.href.match(/^https?:\/\/matrix.to\/#\/(@.+:.+)/);
|
||||
if (userLink !== null) {
|
||||
// convert user link to pill
|
||||
const userId = userLink[1];
|
||||
const pill = {
|
||||
tagName: 'span',
|
||||
attribs: {
|
||||
'data-mx-pill': userId,
|
||||
},
|
||||
};
|
||||
if (userId === initMatrix.matrixClient.getUserId()) {
|
||||
pill.attribs['data-mx-ping'] = undefined;
|
||||
}
|
||||
return pill;
|
||||
}
|
||||
|
||||
const rex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug;
|
||||
const newHref = attribs.href.replace(rex, (match) => `[e-${match.codePointAt(0).toString(16)}]`);
|
||||
|
||||
return {
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
href: newHref,
|
||||
rel: 'noopener',
|
||||
target: '_blank',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function transformImgTag(tagName, attribs) {
|
||||
const { src } = attribs;
|
||||
const mx = initMatrix.matrixClient;
|
||||
return {
|
||||
tagName,
|
||||
attribs: {
|
||||
...attribs,
|
||||
src: src.startsWith('mxc://') ? mx.mxcUrlToHttp(src) : src,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizeCustomHtml(body) {
|
||||
return sanitizeHtml(body, {
|
||||
allowedTags: permittedHtmlTags,
|
||||
allowedAttributes: permittedTagToAttributes,
|
||||
disallowedTagsMode: 'discard',
|
||||
allowedSchemes: urlSchemes,
|
||||
allowedSchemesByTag: {
|
||||
a: urlSchemes,
|
||||
},
|
||||
allowedSchemesAppliedToAttributes: ['href'],
|
||||
allowProtocolRelative: false,
|
||||
allowedClasses: {
|
||||
code: ['language-*'],
|
||||
},
|
||||
allowedStyles: {
|
||||
'*': {
|
||||
color: [/^#(?:[0-9a-fA-F]{3}){1,2}$/],
|
||||
'background-color': [/^#(?:[0-9a-fA-F]{3}){1,2}$/],
|
||||
},
|
||||
},
|
||||
transformTags: {
|
||||
font: transformFontTag,
|
||||
span: transformSpanTag,
|
||||
a: transformATag,
|
||||
img: transformImgTag,
|
||||
},
|
||||
nonTextTags: ['style', 'script', 'textarea', 'option', 'noscript', 'mx-reply'],
|
||||
nestingLimit: MAX_TAG_NESTING,
|
||||
});
|
||||
}
|
||||
|
||||
export function sanitizeText(body) {
|
||||
const tagsToReplace = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
};
|
||||
return body.replace(/[&<>]/g, (tag) => tagsToReplace[tag] || tag);
|
||||
}
|
21
src/util/twemojify.js
Normal file
21
src/util/twemojify.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable import/prefer-default-export */
|
||||
import linkifyHtml from 'linkifyjs/html';
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import { sanitizeText } from './sanitize';
|
||||
|
||||
/**
|
||||
* @param {string} text - text to twemojify
|
||||
* @param {object|undefined} opts - options for tweomoji.parse
|
||||
* @param {boolean} [linkify=false] - convert links to html tags (default: false)
|
||||
* @param {boolean} [sanitize=true] - sanitize html text (default: true)
|
||||
* @returns React component
|
||||
*/
|
||||
export function twemojify(text, opts, linkify = false, sanitize = true) {
|
||||
if (typeof text !== 'string') return text;
|
||||
let content = sanitize ? twemoji.parse(sanitizeText(text), opts) : twemoji.parse(text, opts);
|
||||
if (linkify) {
|
||||
content = linkifyHtml(content, { target: '_blank', rel: 'noreferrer noopener' });
|
||||
}
|
||||
return parse(content);
|
||||
}
|
Loading…
Reference in a new issue