diff --git a/src/app/organisms/room/MembersDrawer.tsx b/src/app/organisms/room/MembersDrawer.tsx index 365dc62..b4ba6b7 100644 --- a/src/app/organisms/room/MembersDrawer.tsx +++ b/src/app/organisms/room/MembersDrawer.tsx @@ -271,7 +271,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) { - {`${millify(room.getJoinedMemberCount(), { precision: 1 })} Members`} + {`${millify(room.getJoinedMemberCount(), { precision: 1, locales: [] })} Members`} diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 09f3326..742971d 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -44,7 +44,6 @@ import { toRem, } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; -import Linkify from 'linkify-react'; import { decryptFile, eventWithShortcode, @@ -76,7 +75,10 @@ import { MessageBadEncryptedContent, MessageNotDecryptedContent, } from '../../components/message'; -import { LINKIFY_OPTS, getReactCustomHtmlParser } from '../../plugins/react-custom-html-parser'; +import { + emojifyAndLinkify, + getReactCustomHtmlParser, +} from '../../plugins/react-custom-html-parser'; import { canEditEvent, decryptAllTimelineEvent, @@ -978,7 +980,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli if (customBody === '') ; return parse(sanitizeCustomHtml(customBody), htmlReactParserOptions); } - return {body}; + return emojifyAndLinkify(body, true); }; const renderRoomMsgContent = useRoomMsgContentRenderer<[EventTimelineSet]>({ diff --git a/src/app/organisms/room/message/AudioContent.tsx b/src/app/organisms/room/message/AudioContent.tsx index b5873f3..eae5447 100644 --- a/src/app/organisms/room/message/AudioContent.tsx +++ b/src/app/organisms/room/message/AudioContent.tsx @@ -44,7 +44,8 @@ export const AudioContent = as<'div', AudioContentProps>( const audioRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(info.duration ?? 0); + // duration in seconds. (NOTE: info.duration is in milliseconds) + const [duration, setDuration] = useState((info.duration ?? 0) / 1000); const getAudioRef = useCallback(() => audioRef.current, []); const { loading } = useMediaLoading(getAudioRef); diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index ae094ef..2b706ed 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -54,7 +54,7 @@ function AppearanceSection() { const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing'); - const [useSystemEmoji, setUseSystemEmoji] = useSetting(settingsAtom, 'useSystemEmoji'); + const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [hideMembershipEvents, setHideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents'); const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents'); @@ -96,14 +96,14 @@ function AppearanceSection() { )} /> setUseSystemEmoji(!useSystemEmoji)} + isActive={twitterEmoji} + onToggle={() => setTwitterEmoji(!twitterEmoji)} /> )} - content={Use system emoji instead of Twitter emojis.} + content={Use Twitter emoji instead of system emoji.} />
@@ -339,6 +339,10 @@ function AboutSection() { {/* eslint-disable-next-line react/jsx-one-expression-per-line */ } The matrix-js-sdk is © The Matrix.org Foundation C.I.C used under the terms of Apache 2.0. +
  • + {/* eslint-disable-next-line react/jsx-one-expression-per-line */ } + The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0. +
  • {/* eslint-disable-next-line react/jsx-one-expression-per-line */ } The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0. diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 09f09d8..478a8a3 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/alt-text */ import React, { ReactEventHandler, Suspense, lazy } from 'react'; -import { +import parse, { Element, Text as DOMText, HTMLReactParserOptions, @@ -16,9 +16,14 @@ import { ErrorBoundary } from 'react-error-boundary'; import * as css from '../styles/CustomHtml.css'; import { getMxIdLocalPart, getRoomWithCanonicalAlias } from '../utils/matrix'; import { getMemberDisplayName } from '../utils/room'; +import { EMOJI_PATTERN, URL_NEG_LB } from '../utils/regex'; +import { sanitizeText } from '../utils/sanitize'; +import { getHexcodeForEmoji, getShortcodeFor } from './emoji'; const ReactPrism = lazy(() => import('./react-prism/ReactPrism')); +const EMOJI_REG = new RegExp(`${URL_NEG_LB}(${EMOJI_PATTERN})`, 'g'); + export const LINKIFY_OPTS: LinkifyOpts = { attributes: { target: '_blank', @@ -27,6 +32,28 @@ export const LINKIFY_OPTS: LinkifyOpts = { validate: { url: (value) => /^(https|http|ftp|mailto|magnet)?:/.test(value), }, + ignoreTags: ['span'], +}; + +const emojifyParserOptions: HTMLReactParserOptions = { + replace: (domNode) => { + if (domNode instanceof DOMText) { + return {domNode.data}; + } + return undefined; + }, +}; + +export const emojifyAndLinkify = (unsafeText: string, linkify?: boolean) => { + const emojifyHtml = sanitizeText(unsafeText).replace( + EMOJI_REG, + (emoji) => + `${emoji}` + ); + + return <>{parse(emojifyHtml, linkify ? emojifyParserOptions : undefined)}; }; export const getReactCustomHtmlParser = ( @@ -45,7 +72,7 @@ export const getReactCustomHtmlParser = ( if (name === 'h1') { return ( - + {domToReact(children, opts)} ); @@ -53,7 +80,7 @@ export const getReactCustomHtmlParser = ( if (name === 'h2') { return ( - + {domToReact(children, opts)} ); @@ -61,7 +88,7 @@ export const getReactCustomHtmlParser = ( if (name === 'h3') { return ( - + {domToReact(children, opts)} ); @@ -69,7 +96,7 @@ export const getReactCustomHtmlParser = ( if (name === 'h4') { return ( - + {domToReact(children, opts)} ); @@ -77,7 +104,7 @@ export const getReactCustomHtmlParser = ( if (name === 'h5') { return ( - + {domToReact(children, opts)} ); @@ -85,7 +112,7 @@ export const getReactCustomHtmlParser = ( if (name === 'h6') { return ( - + {domToReact(children, opts)} ); @@ -93,7 +120,7 @@ export const getReactCustomHtmlParser = ( if (name === 'p') { return ( - + {domToReact(children, opts)} ); @@ -101,7 +128,7 @@ export const getReactCustomHtmlParser = ( if (name === 'pre') { return ( - + + {domToReact(children, opts)} ); @@ -125,14 +152,14 @@ export const getReactCustomHtmlParser = ( if (name === 'ul') { return ( -
      +
        {domToReact(children, opts)}
      ); } if (name === 'ol') { return ( -
        +
          {domToReact(children, opts)}
        ); @@ -240,29 +267,28 @@ export const getReactCustomHtmlParser = ( if (htmlSrc && props.src.startsWith('mxc://') === false) { return ( - {props.alt && htmlSrc} + {props.alt || props.title || htmlSrc} ); } if (htmlSrc && 'data-mx-emoticon' in props) { return ( - - + + ); } - if (htmlSrc) return ; + if (htmlSrc) return ; } } - if ( - domNode instanceof DOMText && - !(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'code') && - !(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'a') - ) { - return {domNode.data}; + if (domNode instanceof DOMText) { + const linkify = + !(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'code') && + !(domNode.parent && 'name' in domNode.parent && domNode.parent.name === 'a'); + return emojifyAndLinkify(domNode.data, linkify); } return undefined; }, diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 26e3431..3a7832c 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -9,7 +9,7 @@ export interface Settings { useSystemTheme: boolean; isMarkdown: boolean; editorToolbar: boolean; - useSystemEmoji: boolean; + twitterEmoji: boolean; isPeopleDrawer: boolean; memberSortFilterIndex: number; @@ -30,7 +30,7 @@ const defaultSettings: Settings = { useSystemTheme: true, isMarkdown: false, editorToolbar: false, - useSystemEmoji: false, + twitterEmoji: false, isPeopleDrawer: true, memberSortFilterIndex: 0, diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts index 2a06c0f..076bbb6 100644 --- a/src/app/styles/CustomHtml.css.ts +++ b/src/app/styles/CustomHtml.css.ts @@ -187,11 +187,11 @@ export const Emoticon = recipe({ height: '1em', minWidth: '1em', - fontSize: '1.47em', + fontSize: '1.33em', lineHeight: '1em', verticalAlign: 'middle', position: 'relative', - top: '-0.32em', + top: '-0.35em', borderRadius: config.radii.R300, }, ], diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index 77db411..e9be6b1 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -24,12 +24,12 @@ import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; function SystemEmojiFeature() { - const [systemEmoji] = useSetting(settingsAtom, 'useSystemEmoji'); + const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); - if (systemEmoji) { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji_DISABLED'); - } else { + if (twitterEmoji) { document.documentElement.style.setProperty('--font-emoji', 'Twemoji'); + } else { + document.documentElement.style.setProperty('--font-emoji', 'Twemoji_DISABLED'); } return null; diff --git a/src/app/utils/regex.ts b/src/app/utils/regex.ts new file mode 100644 index 0000000..73da664 --- /dev/null +++ b/src/app/utils/regex.ts @@ -0,0 +1,4 @@ +export const URL_NEG_LB = '(?