Compare commits
14 commits
a2b8b127f4
...
66dbb1616d
Author | SHA1 | Date | |
---|---|---|---|
|
66dbb1616d | ||
|
212f18b69c | ||
|
197b15eb6d | ||
|
aec2c44c29 | ||
|
291dad7ef8 | ||
|
d193645b11 | ||
|
a7a4b5b0f1 | ||
|
745cd87b7f | ||
|
b86967fc3c | ||
|
870de09b0e | ||
|
7dcdf98b4e | ||
|
34f4afab67 | ||
|
0d0ea8f09f | ||
|
5bc5e3cbd2 |
49 changed files with 3546 additions and 5772 deletions
|
@ -12,7 +12,7 @@
|
||||||
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
|
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch. This is a fork of the original project, located [here](https://github.com/cinnyapp/cinny). This fork aims to solve various annoyances and add new features. We're planning on upstreaming these changes once they're stable enough. We're not affiliated with the official Cinny project in any way.
|
||||||
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
|
||||||
- [Contributing](./CONTRIBUTING.md)
|
- [Contributing](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"defaultHomeserver": 3,
|
"defaultHomeserver": 0,
|
||||||
"homeserverList": [
|
"homeserverList": [
|
||||||
|
"http://localhost:3000",
|
||||||
"converser.eu",
|
"converser.eu",
|
||||||
"envs.net",
|
"envs.net",
|
||||||
"halogen.city",
|
"halogen.city",
|
||||||
|
|
82
index.html
82
index.html
|
@ -1,5 +1,6 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
@ -7,22 +8,13 @@
|
||||||
<title>Cinny</title>
|
<title>Cinny</title>
|
||||||
<meta name="name" content="Cinny" />
|
<meta name="name" content="Cinny" />
|
||||||
<meta name="author" content="Ajay Bura" />
|
<meta name="author" content="Ajay Bura" />
|
||||||
<meta
|
<meta name="description"
|
||||||
name="description"
|
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source." />
|
||||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
<meta name="keywords" content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element" />
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
name="keywords"
|
|
||||||
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<meta property="og:title" content="Cinny" />
|
<meta property="og:title" content="Cinny" />
|
||||||
<meta property="og:url" content="https://cinny.in" />
|
<meta property="og:description"
|
||||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source." />
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source."
|
|
||||||
/>
|
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||||
|
@ -34,57 +26,18 @@
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
|
||||||
<link
|
<link rel="apple-touch-icon" sizes="57x57" href="./public/res/apple/apple-touch-icon-57x57.png" />
|
||||||
rel="apple-touch-icon"
|
<link rel="apple-touch-icon" sizes="60x60" href="./public/res/apple/apple-touch-icon-60x60.png" />
|
||||||
sizes="57x57"
|
<link rel="apple-touch-icon" sizes="72x72" href="./public/res/apple/apple-touch-icon-72x72.png" />
|
||||||
href="./public/res/apple/apple-touch-icon-57x57.png"
|
<link rel="apple-touch-icon" sizes="76x76" href="./public/res/apple/apple-touch-icon-76x76.png" />
|
||||||
/>
|
<link rel="apple-touch-icon" sizes="114x114" href="./public/res/apple/apple-touch-icon-114x114.png" />
|
||||||
<link
|
<link rel="apple-touch-icon" sizes="120x120" href="./public/res/apple/apple-touch-icon-120x120.png" />
|
||||||
rel="apple-touch-icon"
|
<link rel="apple-touch-icon" sizes="144x144" href="./public/res/apple/apple-touch-icon-144x144.png" />
|
||||||
sizes="60x60"
|
<link rel="apple-touch-icon" sizes="152x152" href="./public/res/apple/apple-touch-icon-152x152.png" />
|
||||||
href="./public/res/apple/apple-touch-icon-60x60.png"
|
<link rel="apple-touch-icon" sizes="167x167" href="./public/res/apple/apple-touch-icon-167x167.png" />
|
||||||
/>
|
<link rel="apple-touch-icon" sizes="180x180" href="./public/res/apple/apple-touch-icon-180x180.png" />
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="72x72"
|
|
||||||
href="./public/res/apple/apple-touch-icon-72x72.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="76x76"
|
|
||||||
href="./public/res/apple/apple-touch-icon-76x76.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="114x114"
|
|
||||||
href="./public/res/apple/apple-touch-icon-114x114.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="120x120"
|
|
||||||
href="./public/res/apple/apple-touch-icon-120x120.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="144x144"
|
|
||||||
href="./public/res/apple/apple-touch-icon-144x144.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="152x152"
|
|
||||||
href="./public/res/apple/apple-touch-icon-152x152.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="167x167"
|
|
||||||
href="./public/res/apple/apple-touch-icon-167x167.png"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="./public/res/apple/apple-touch-icon-180x180.png"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="appBody">
|
<body id="appBody">
|
||||||
<script>
|
<script>
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
|
@ -98,4 +51,5 @@
|
||||||
</audio>
|
</audio>
|
||||||
<script type="module" src="./src/index.jsx"></script>
|
<script type="module" src="./src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
5331
package-lock.json
generated
5331
package-lock.json
generated
File diff suppressed because it is too large
Load diff
60
package.json
60
package.json
|
@ -9,6 +9,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
"lint": "yarn check:eslint && yarn check:prettier",
|
"lint": "yarn check:eslint && yarn check:prettier",
|
||||||
"check:eslint": "eslint src/*",
|
"check:eslint": "eslint src/*",
|
||||||
"check:prettier": "prettier --check .",
|
"check:prettier": "prettier --check .",
|
||||||
|
@ -19,59 +20,54 @@
|
||||||
"author": "Ajay Bura",
|
"author": "Ajay Bura",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "4.5.14",
|
"@fontsource/inter": "4.5.15",
|
||||||
"@fontsource/roboto": "4.5.8",
|
"@fontsource/roboto": "4.5.8",
|
||||||
"@khanacademy/simple-markdown": "0.8.6",
|
"@khanacademy/simple-markdown": "0.8.6",
|
||||||
"@matrix-org/olm": "3.2.14",
|
"@matrix-org/olm": "3.2.14",
|
||||||
"@tippyjs/react": "4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.5",
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"dateformat": "5.0.3",
|
"dateformat": "5.0.3",
|
||||||
"emojibase-data": "7.0.1",
|
"emojibase-data": "7.0.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.4",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "3.0.4",
|
"html-react-parser": "3.0.13",
|
||||||
"katex": "0.16.4",
|
"linkify-html": "4.1.0",
|
||||||
"linkify-html": "4.0.2",
|
"linkifyjs": "4.1.0",
|
||||||
"linkifyjs": "4.0.2",
|
"matrix-js-sdk": "23.5.0",
|
||||||
"matrix-js-sdk": "22.0.0",
|
"preact": "10.13.1",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
|
||||||
"react-autosize-textarea": "7.1.0",
|
"react-autosize-textarea": "7.1.0",
|
||||||
"react-blurhash": "0.2.0",
|
"react-blurhash": "0.3.0",
|
||||||
"react-dnd": "15.1.2",
|
"react-dnd": "16.0.1",
|
||||||
"react-dnd-html5-backend": "15.1.3",
|
"react-dnd-html5-backend": "16.0.1",
|
||||||
"react-dom": "17.0.2",
|
|
||||||
"react-google-recaptcha": "2.1.0",
|
|
||||||
"react-modal": "3.16.1",
|
"react-modal": "3.16.1",
|
||||||
"sanitize-html": "2.8.0",
|
"sanitize-html": "2.10.0",
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"twemoji": "14.0.2"
|
"twemoji": "14.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
|
"@preact/preset-vite": "2.5.0",
|
||||||
"@rollup/plugin-inject": "5.0.3",
|
"@rollup/plugin-inject": "5.0.3",
|
||||||
"@rollup/plugin-wasm": "6.1.1",
|
"@rollup/plugin-wasm": "6.1.2",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.15.5",
|
||||||
"@types/react": "18.0.26",
|
"@typescript-eslint/eslint-plugin": "5.56.0",
|
||||||
"@types/react-dom": "18.0.9",
|
"@typescript-eslint/parser": "5.56.0",
|
||||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
|
||||||
"@typescript-eslint/parser": "5.46.1",
|
|
||||||
"@vitejs/plugin-react": "3.0.0",
|
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"eslint": "8.29.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"eslint-config-prettier": "8.8.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||||
"eslint-plugin-react": "7.31.11",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"mini-svg-data-uri": "1.4.4",
|
"mini-svg-data-uri": "1.4.4",
|
||||||
"prettier": "2.8.1",
|
"prettier": "2.8.6",
|
||||||
"sass": "1.56.2",
|
"sass": "1.59.3",
|
||||||
"typescript": "4.9.4",
|
"typescript": "5.0.2",
|
||||||
"vite": "4.0.1",
|
"vite": "4.2.1",
|
||||||
"vite-plugin-static-copy": "0.13.0"
|
"vite-plugin-static-copy": "0.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ const Avatar = React.forwardRef(({
|
||||||
iconSrc !== null
|
iconSrc !== null
|
||||||
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||||
: text !== null && (
|
: text !== null && (
|
||||||
<Text variant={textSize} primary>
|
<Text variant={textSize} monospace>
|
||||||
{twemojify(avatarInitials(text))}
|
{twemojify(avatarInitials(text))}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: var(--bo-radius);
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__large {
|
&__large {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './ContextMenu.scss';
|
import './ContextMenu.scss';
|
||||||
|
|
||||||
import Tippy from '@tippyjs/react';
|
import Tippy from '@tippyjs/react';
|
||||||
import 'tippy.js/animations/scale-extreme.css';
|
import 'tippy.js/animations/scale-subtle.css';
|
||||||
|
|
||||||
import Text from '../text/Text';
|
import Text from '../text/Text';
|
||||||
import Button from '../button/Button';
|
import Button from '../button/Button';
|
||||||
|
@ -13,8 +13,12 @@ function ContextMenu({
|
||||||
content, placement, maxWidth, render, afterToggle,
|
content, placement, maxWidth, render, afterToggle,
|
||||||
}) {
|
}) {
|
||||||
const [isVisible, setVisibility] = useState(false);
|
const [isVisible, setVisibility] = useState(false);
|
||||||
const showMenu = () => setVisibility(true);
|
const showMenu = useCallback(() => {
|
||||||
const hideMenu = () => setVisibility(false);
|
setVisibility(true);
|
||||||
|
});
|
||||||
|
const hideMenu = useCallback(() => {
|
||||||
|
setVisibility(false);
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (afterToggle !== null) afterToggle(isVisible);
|
if (afterToggle !== null) afterToggle(isVisible);
|
||||||
|
@ -22,7 +26,7 @@ function ContextMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tippy
|
<Tippy
|
||||||
animation="scale-extreme"
|
animation="scale-subtle"
|
||||||
className="context-menu"
|
className="context-menu"
|
||||||
visible={isVisible}
|
visible={isVisible}
|
||||||
onClickOutside={hideMenu}
|
onClickOutside={hideMenu}
|
||||||
|
@ -31,7 +35,7 @@ function ContextMenu({
|
||||||
interactive
|
interactive
|
||||||
arrow={false}
|
arrow={false}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
duration={200}
|
duration={150}
|
||||||
>
|
>
|
||||||
{render(isVisible ? hideMenu : showMenu)}
|
{render(isVisible ? hideMenu : showMenu)}
|
||||||
</Tippy>
|
</Tippy>
|
||||||
|
|
6
src/app/atoms/loading-text/LoadingText.jsx
Normal file
6
src/app/atoms/loading-text/LoadingText.jsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import Text from '../text/Text';
|
||||||
|
import './LoadingText.scss';
|
||||||
|
|
||||||
|
export function LoadingText() {
|
||||||
|
return <Text className='loading-text'>Loading...</Text>
|
||||||
|
}
|
5
src/app/atoms/loading-text/LoadingText.scss
Normal file
5
src/app/atoms/loading-text/LoadingText.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.loading-text {
|
||||||
|
display: block;
|
||||||
|
padding: var(--sp-tight);
|
||||||
|
color: var(--tc-surface-low);
|
||||||
|
}
|
|
@ -1,33 +0,0 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import './Math.scss';
|
|
||||||
|
|
||||||
import katex from 'katex';
|
|
||||||
import 'katex/dist/katex.min.css';
|
|
||||||
|
|
||||||
import 'katex/dist/contrib/copy-tex';
|
|
||||||
|
|
||||||
const Math = React.memo(({
|
|
||||||
content, throwOnError, errorColor, displayMode,
|
|
||||||
}) => {
|
|
||||||
const ref = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
katex.render(content, ref.current, { throwOnError, errorColor, displayMode });
|
|
||||||
}, [content, throwOnError, errorColor, displayMode]);
|
|
||||||
|
|
||||||
return <span ref={ref} />;
|
|
||||||
});
|
|
||||||
Math.defaultProps = {
|
|
||||||
throwOnError: null,
|
|
||||||
errorColor: null,
|
|
||||||
displayMode: null,
|
|
||||||
};
|
|
||||||
Math.propTypes = {
|
|
||||||
content: PropTypes.string.isRequired,
|
|
||||||
throwOnError: PropTypes.bool,
|
|
||||||
errorColor: PropTypes.string,
|
|
||||||
displayMode: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Math;
|
|
|
@ -1,3 +0,0 @@
|
||||||
.katex-display {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
|
@ -39,20 +39,22 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Overlay {
|
.ReactModal__Overlay {
|
||||||
animation: raw-modal--overlay 150ms;
|
animation: raw-modal--overlay 210ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.77,0,0.18,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Content {
|
.ReactModal__Content {
|
||||||
animation: raw-modal--content 150ms;
|
animation: raw-modal--content 210ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.77,0,0.18,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes raw-modal--content {
|
@keyframes raw-modal--content {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(100px);
|
transform: scale(0.90);
|
||||||
opacity: .5;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(0);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,10 +41,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin scroll__h {
|
@mixin scroll__h {
|
||||||
overflow-x: scroll;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
@mixin scroll__v {
|
@mixin scroll__v {
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@mixin scroll--auto-hide {
|
@mixin scroll--auto-hide {
|
||||||
@extend .firefox-scrollbar--transparent;
|
@extend .firefox-scrollbar--transparent;
|
||||||
|
|
|
@ -4,13 +4,14 @@ import './Text.scss';
|
||||||
|
|
||||||
function Text({
|
function Text({
|
||||||
className, style, variant, weight,
|
className, style, variant, weight,
|
||||||
primary, span, children,
|
primary, monospace, span, children,
|
||||||
}) {
|
}) {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
if (className) classes.push(className);
|
if (className) classes.push(className);
|
||||||
|
|
||||||
classes.push(`text text-${variant} text-${weight}`);
|
classes.push(`text text-${variant} text-${weight}`);
|
||||||
if (primary) classes.push('font-primary');
|
if (primary) classes.push('font-primary');
|
||||||
|
if (monospace) classes.push('font-monospace');
|
||||||
|
|
||||||
const textClass = classes.join(' ');
|
const textClass = classes.join(' ');
|
||||||
if (span) return <span className={textClass} style={style}>{ children }</span>;
|
if (span) return <span className={textClass} style={style}>{ children }</span>;
|
||||||
|
@ -26,6 +27,7 @@ Text.defaultProps = {
|
||||||
variant: 'b1',
|
variant: 'b1',
|
||||||
weight: 'normal',
|
weight: 'normal',
|
||||||
primary: false,
|
primary: false,
|
||||||
|
monospace: false,
|
||||||
span: false,
|
span: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -35,6 +37,7 @@ Text.propTypes = {
|
||||||
variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']),
|
variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']),
|
||||||
weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']),
|
weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']),
|
||||||
primary: PropTypes.bool,
|
primary: PropTypes.bool,
|
||||||
|
monospace: PropTypes.bool,
|
||||||
span: PropTypes.bool,
|
span: PropTypes.bool,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
content-visibility: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -6,29 +6,24 @@ import { isInSameDay } from '../../../util/common';
|
||||||
|
|
||||||
function Time({ timestamp, fullTime }) {
|
function Time({ timestamp, fullTime }) {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
|
let formattedDate;
|
||||||
|
|
||||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
if (fullTime) {
|
||||||
let formattedDate = formattedFullTime;
|
formattedDate = formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT')
|
||||||
|
} else {
|
||||||
if (!fullTime) {
|
|
||||||
const compareDate = new Date();
|
const compareDate = new Date();
|
||||||
const isToday = isInSameDay(date, compareDate);
|
const isToday = isInSameDay(date, compareDate);
|
||||||
compareDate.setDate(compareDate.getDate() - 1);
|
compareDate.setDate(compareDate.getDate() - 1);
|
||||||
const isYesterday = isInSameDay(date, compareDate);
|
const isYesterday = isInSameDay(date, compareDate);
|
||||||
|
|
||||||
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy');
|
formattedDate = dateFormat(date, isToday || isYesterday ? 'hh:MM TT' : 'dd/mm/yyyy hh:MM TT');
|
||||||
if (isYesterday) {
|
if (isYesterday) {
|
||||||
formattedDate = `Yesterday, ${formattedDate}`;
|
formattedDate = `Yesterday, ${formattedDate}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<time
|
<time dateTime={date.toISOString()}>{formattedDate}</time>
|
||||||
dateTime={date.toISOString()}
|
|
||||||
title={formattedFullTime}
|
|
||||||
>
|
|
||||||
{formattedDate}
|
|
||||||
</time>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import './Tooltip.scss';
|
||||||
import Tippy from '@tippyjs/react';
|
import Tippy from '@tippyjs/react';
|
||||||
|
|
||||||
function Tooltip({
|
function Tooltip({
|
||||||
className, placement, content, delay, children,
|
className, placement, content, children,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tippy
|
<Tippy
|
||||||
|
@ -14,8 +14,8 @@ function Tooltip({
|
||||||
arrow={false}
|
arrow={false}
|
||||||
maxWidth={250}
|
maxWidth={250}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
delay={delay}
|
duration={[75, 0]}
|
||||||
duration={[100, 0]}
|
animation="scale-subtle"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Tippy>
|
</Tippy>
|
||||||
|
@ -25,14 +25,12 @@ function Tooltip({
|
||||||
Tooltip.defaultProps = {
|
Tooltip.defaultProps = {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
className: '',
|
className: '',
|
||||||
delay: [200, 0],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
Tooltip.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
placement: PropTypes.string,
|
placement: PropTypes.string,
|
||||||
content: PropTypes.node.isRequired,
|
content: PropTypes.node.isRequired,
|
||||||
delay: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -85,12 +85,10 @@ const MessageHeader = React.memo(({
|
||||||
<span>{twemojify(username)}</span>
|
<span>{twemojify(username)}</span>
|
||||||
<span>{twemojify(userId)}</span>
|
<span>{twemojify(userId)}</span>
|
||||||
</Text>
|
</Text>
|
||||||
<div className="message__time">
|
<Text className="message__time" variant="b3">
|
||||||
<Text variant="b3">
|
|
||||||
<Time timestamp={timestamp} fullTime={fullTime} />
|
<Time timestamp={timestamp} fullTime={fullTime} />
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
));
|
));
|
||||||
MessageHeader.defaultProps = {
|
MessageHeader.defaultProps = {
|
||||||
fullTime: false,
|
fullTime: false,
|
||||||
|
@ -233,7 +231,7 @@ const MessageBody = React.memo(({
|
||||||
if (content.type === 'img') {
|
if (content.type === 'img') {
|
||||||
// If this messages contains only a single (inline) image
|
// If this messages contains only a single (inline) image
|
||||||
emojiOnly = true;
|
emojiOnly = true;
|
||||||
} else if (content.constructor.name === 'Array') {
|
} else if (content.constructor && content.constructor.name === 'Array') {
|
||||||
// Otherwise, it might be an array of images / texb
|
// Otherwise, it might be an array of images / texb
|
||||||
|
|
||||||
// Count the number of emojis
|
// Count the number of emojis
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
& .message__profile {
|
& .message__profile {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--tc-surface-high);
|
color: var(--tc-surface-high);
|
||||||
@include dir.side(margin, 0, var(--sp-tight));
|
@include dir.side(margin, 0, var(--sp-ultra-tight));
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
@extend .cp-txt__ellipsis;
|
@extend .cp-txt__ellipsis;
|
||||||
|
@ -128,15 +128,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& .message__time {
|
& .message__time {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
& > .text {
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--tc-surface-low);
|
color: var(--tc-surface-low);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.message__reply {
|
.message__reply {
|
||||||
&-wrapper {
|
&-wrapper {
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
@use '../../partials/text';
|
@use '../../partials/text';
|
||||||
|
|
||||||
.people-selector {
|
.people-selector {
|
||||||
width: 100%;
|
|
||||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||||
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin: var(--sp-extra-tight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
& .segmented-controls {
|
& .segmented-controls {
|
||||||
@include dir.side(margin, 0, var(--sp-normal));
|
margin: var(--sp-extra-tight);
|
||||||
& > button {
|
& > button {
|
||||||
padding: var(--sp-ultra-tight) 0;
|
padding: var(--sp-ultra-tight) 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,6 +177,11 @@ function RoomPermissions({ roomId }) {
|
||||||
const mx = initMatrix.matrixClient;
|
const mx = initMatrix.matrixClient;
|
||||||
const room = mx.getRoom(roomId);
|
const room = mx.getRoom(roomId);
|
||||||
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
||||||
|
|
||||||
|
if (!pLEvent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const permissions = pLEvent.getContent();
|
const permissions = pLEvent.getContent();
|
||||||
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
||||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
|
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
|
||||||
|
|
|
@ -2,16 +2,15 @@
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.room-profile {
|
.room-profile {
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
@extend .cp-fx__row;
|
@extend .cp-fx__row;
|
||||||
|
align-items: center;
|
||||||
& .avatar-container {
|
& .avatar-container {
|
||||||
min-width: var(--av-large);
|
min-width: var(--av-large);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__display {
|
&__display {
|
||||||
align-self: flex-end;
|
|
||||||
@include dir.side(margin, var(--sp-loose), 0);
|
@include dir.side(margin, var(--sp-loose), 0);
|
||||||
|
|
||||||
& > div:first-child {
|
& > div:first-child {
|
||||||
|
|
|
@ -54,9 +54,6 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
& .message__time {
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,8 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './EmojiBoard.scss';
|
import './EmojiBoard.scss';
|
||||||
|
|
||||||
import parse from 'html-react-parser';
|
import parse from "html-react-parser";
|
||||||
import twemoji from 'twemoji';
|
|
||||||
import { emojiGroups, emojis } from './emoji';
|
import { emojiGroups, emojis } from './emoji';
|
||||||
import { getRelevantPacks } from './custom-emoji';
|
import { getRelevantPacks } from './custom-emoji';
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
@ -13,7 +12,6 @@ import cons from '../../../client/state/cons';
|
||||||
import navigation from '../../../client/state/navigation';
|
import navigation from '../../../client/state/navigation';
|
||||||
import AsyncSearch from '../../../util/AsyncSearch';
|
import AsyncSearch from '../../../util/AsyncSearch';
|
||||||
import { addRecentEmoji, getRecentEmojis } from './recent';
|
import { addRecentEmoji, getRecentEmojis } from './recent';
|
||||||
import { TWEMOJI_BASE_URL } from '../../../util/twemojify';
|
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||||
|
@ -49,17 +47,7 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
||||||
<span key={emojiIndex}>
|
<span key={emojiIndex}>
|
||||||
{emoji.hexcode ? (
|
{emoji.hexcode ? (
|
||||||
// This is a unicode emoji, and should be rendered with twemoji
|
// This is a unicode emoji, and should be rendered with twemoji
|
||||||
parse(
|
<span className="emoji" unicode={emoji.unicode} shortcodes={emoji.shortcode} data-mx-emoticon={emoji.mxc}>{ emoji.unicode }</span>
|
||||||
twemoji.parse(emoji.unicode, {
|
|
||||||
attributes: () => ({
|
|
||||||
unicode: emoji.unicode,
|
|
||||||
shortcodes: emoji.shortcodes?.toString(),
|
|
||||||
hexcode: emoji.hexcode,
|
|
||||||
loading: 'lazy',
|
|
||||||
}),
|
|
||||||
base: TWEMOJI_BASE_URL,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
// This is a custom emoji, and should be render as an mxc
|
// This is a custom emoji, and should be render as an mxc
|
||||||
<img
|
<img
|
||||||
|
@ -143,7 +131,7 @@ function SearchedEmoji() {
|
||||||
|
|
||||||
function EmojiBoard({ onSelect, searchRef }) {
|
function EmojiBoard({ onSelect, searchRef }) {
|
||||||
const scrollEmojisRef = useRef(null);
|
const scrollEmojisRef = useRef(null);
|
||||||
const emojiInfo = useRef(null);
|
const [emojiInfo, setEmojiInfo] = useState(null);
|
||||||
|
|
||||||
function isTargetNotEmoji(target) {
|
function isTargetNotEmoji(target) {
|
||||||
return target.classList.contains('emoji') === false;
|
return target.classList.contains('emoji') === false;
|
||||||
|
@ -171,34 +159,15 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||||
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
|
if (emoji.hexcode) addRecentEmoji(emoji.unicode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEmojiInfo(emoji) {
|
|
||||||
const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild;
|
|
||||||
const infoShortcode = emojiInfo.current.lastElementChild;
|
|
||||||
|
|
||||||
infoEmoji.src = emoji.src;
|
|
||||||
infoEmoji.alt = emoji.unicode;
|
|
||||||
infoShortcode.textContent = `:${emoji.shortcode}:`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hoverEmoji(e) {
|
function hoverEmoji(e) {
|
||||||
if (isTargetNotEmoji(e.target)) return;
|
if (isTargetNotEmoji(e.target)) return;
|
||||||
|
|
||||||
const emoji = e.target;
|
const emoji = e.target;
|
||||||
const { shortcodes, unicode } = getEmojiDataFromTarget(emoji);
|
const { shortcodes, unicode } = getEmojiDataFromTarget(emoji);
|
||||||
const { src } = e.target;
|
|
||||||
|
|
||||||
if (typeof shortcodes === 'undefined') {
|
|
||||||
searchRef.current.placeholder = 'Search';
|
|
||||||
setEmojiInfo({
|
|
||||||
unicode: '🙂',
|
|
||||||
shortcode: 'slight_smile',
|
|
||||||
src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (searchRef.current.placeholder === shortcodes[0]) return;
|
if (searchRef.current.placeholder === shortcodes[0]) return;
|
||||||
searchRef.current.setAttribute('placeholder', shortcodes[0]);
|
searchRef.current.setAttribute('placeholder', shortcodes[0]);
|
||||||
setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
|
setEmojiInfo({ shortcode: shortcodes[0], unicode });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchChange() {
|
function handleSearchChange() {
|
||||||
|
@ -339,9 +308,9 @@ function EmojiBoard({ onSelect, searchRef }) {
|
||||||
</div>
|
</div>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
<div ref={emojiInfo} className="emoji-board__content__info">
|
<div className="emoji-board__content__info">
|
||||||
<div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
|
<div><span className="emoji">{ emojiInfo ? emojiInfo.unicode : '' }</span></div>
|
||||||
<Text>:slight_smile:</Text>
|
<Text>{emojiInfo ? emojiInfo.shortcode : ''}</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
.emoji-board {
|
.emoji-board {
|
||||||
--emoji-board-height: 390px;
|
--emoji-board-height: 390px;
|
||||||
--emoji-board-width: 286px;
|
--emoji-board-width: 218px;
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 90vw;
|
max-width: 90vw;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
|
@ -121,6 +121,7 @@
|
||||||
@include dir.side(margin, var(--left-margin), var(--right-margin));
|
@include dir.side(margin, var(--left-margin), var(--right-margin));
|
||||||
}
|
}
|
||||||
& .emoji {
|
& .emoji {
|
||||||
|
display: block;
|
||||||
max-width: 38px;
|
max-width: 38px;
|
||||||
max-height: 38px;
|
max-height: 38px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -132,13 +132,13 @@ function DrawerHeader({ selectedTab, spaceId }) {
|
||||||
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
|
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
|
||||||
>
|
>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
|
<Text primary>{twemojify(spaceName)}</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
<RawIcon size="small" src={ChevronBottomIC} />
|
<RawIcon size="small" src={ChevronBottomIC} />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="s1" weight="medium" primary>{tabName}</Text>
|
<Text primary>{tabName}</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& .room-selector {
|
& .room-selector {
|
||||||
width: calc(100% - var(--sp-extra-tight));
|
margin: var(--sp-extra-tight);
|
||||||
@include dir.side(margin, auto, 0);
|
margin-top: var(--sp-ultra-tight);
|
||||||
|
margin-bottom: var(--sp-ultra-tight);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -62,7 +62,7 @@
|
||||||
.sidebar__cross-signin-alert .avatar-container {
|
.sidebar__cross-signin-alert .avatar-container {
|
||||||
box-shadow: var(--bs-danger-border);
|
box-shadow: var(--bs-danger-border);
|
||||||
animation-name: pushRight;
|
animation-name: pushRight;
|
||||||
animation-duration: 400ms;
|
animation-duration: 250ms;
|
||||||
animation-iteration-count: infinite;
|
animation-iteration-count: infinite;
|
||||||
animation-direction: alternate;
|
animation-direction: alternate;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,10 @@
|
||||||
.profile-viewer {
|
.profile-viewer {
|
||||||
&__user {
|
&__user {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding-bottom: var(--sp-normal);
|
padding-bottom: var(--sp-normal);
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
align-self: flex-end;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,7 @@ function PeopleDrawer({ roomId }) {
|
||||||
<div className="people-drawer">
|
<div className="people-drawer">
|
||||||
<Header>
|
<Header>
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="s1" primary>
|
<Text span primary>
|
||||||
People
|
People
|
||||||
<Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
|
<Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -68,15 +68,13 @@
|
||||||
& .people-selector {
|
& .people-selector {
|
||||||
padding: var(--sp-extra-tight);
|
padding: var(--sp-extra-tight);
|
||||||
border-radius: var(--bo-radius);
|
border-radius: var(--bo-radius);
|
||||||
&__container {
|
|
||||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& .segmented-controls {
|
& .segmented-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: var(--sp-extra-tight);
|
margin: var(--sp-extra-tight);
|
||||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
margin-top: 0;
|
||||||
|
margin-bottom: var(--sp-tight);
|
||||||
}
|
}
|
||||||
& .segment-btn {
|
& .segment-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -40,6 +40,5 @@
|
||||||
min-height: 85px;
|
min-height: 85px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border-top: 1px solid var(--bg-surface-border);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,10 +2,8 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './RoomViewCmdBar.scss';
|
import './RoomViewCmdBar.scss';
|
||||||
import parse from 'html-react-parser';
|
|
||||||
import twemoji from 'twemoji';
|
|
||||||
|
|
||||||
import { twemojify, TWEMOJI_BASE_URL } from '../../../util/twemojify';
|
import { singleEmojiToJSX, twemojify } from '../../../util/twemojify';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
||||||
|
@ -53,15 +51,7 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||||
|
|
||||||
// Renders a small Twemoji
|
// Renders a small Twemoji
|
||||||
function renderTwemoji(emoji) {
|
function renderTwemoji(emoji) {
|
||||||
return parse(
|
return singleEmojiToJSX(emoji);
|
||||||
twemoji.parse(emoji.unicode, {
|
|
||||||
attributes: () => ({
|
|
||||||
unicode: emoji.unicode,
|
|
||||||
shortcodes: emoji.shortcodes?.toString(),
|
|
||||||
}),
|
|
||||||
base: TWEMOJI_BASE_URL,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render a custom emoji
|
// Render a custom emoji
|
||||||
|
@ -69,6 +59,9 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className="emoji"
|
className="emoji"
|
||||||
|
draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
src={mx.mxcUrlToHttp(emoji.mxc)}
|
src={mx.mxcUrlToHttp(emoji.mxc)}
|
||||||
data-mx-emoticon=""
|
data-mx-emoticon=""
|
||||||
alt={`:${emoji.shortcode}:`}
|
alt={`:${emoji.shortcode}:`}
|
||||||
|
|
|
@ -90,9 +90,9 @@ function RoomViewHeader({ roomId }) {
|
||||||
>
|
>
|
||||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
|
<Text weight="bold" primary>{twemojify(roomName)}</Text>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
<RawIcon src={ChevronBottomIC} />
|
<RawIcon size="extra-small" src={ChevronBottomIC} />
|
||||||
</button>
|
</button>
|
||||||
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
|
{mx.isRoomEncrypted(roomId) === false && <IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />}
|
||||||
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
<IconButton className="room-header__drawer-btn" onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||||
|
|
|
@ -361,12 +361,12 @@ function RoomViewInput({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
|
|
||||||
<input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
|
|
||||||
<IconButton onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
|
|
||||||
</div>
|
|
||||||
<div ref={inputBaseRef} className="room-input__input-container">
|
<div ref={inputBaseRef} className="room-input__input-container">
|
||||||
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
|
{roomTimeline.isEncrypted() && <RawIcon size="extra-small" src={ShieldIC} />}
|
||||||
|
<div className={`room-input__option-container${attachment === null ? '' : ' room-attachment__option'}`}>
|
||||||
|
<input onChange={uploadFileChange} style={{ display: 'none' }} ref={uploadInputRef} type="file" />
|
||||||
|
<IconButton size="small" onClick={handleUploadClick} tooltip={attachment === null ? 'Upload' : 'Cancel'} src={CirclePlusIC} />
|
||||||
|
</div>
|
||||||
<ScrollView autoHide>
|
<ScrollView autoHide>
|
||||||
<Text className="room-input__textarea-wrapper">
|
<Text className="room-input__textarea-wrapper">
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
|
@ -380,7 +380,6 @@ function RoomViewInput({
|
||||||
/>
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</div>
|
|
||||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -404,6 +403,7 @@ function RoomViewInput({
|
||||||
}}
|
}}
|
||||||
tooltip="Sticker"
|
tooltip="Sticker"
|
||||||
src={StickerIC}
|
src={StickerIC}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
@ -414,8 +414,9 @@ function RoomViewInput({
|
||||||
}}
|
}}
|
||||||
tooltip="Emoji"
|
tooltip="Emoji"
|
||||||
src={EmojiIC}
|
src={EmojiIC}
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
<IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
@use '../../partials/dir';
|
@use '../../partials/dir';
|
||||||
|
|
||||||
.room-input {
|
.room-input {
|
||||||
padding: var(--sp-extra-tight) calc(var(--sp-normal) - 2px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&__alert {
|
&__alert {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
background-color: var(--bg-surface-low);
|
background-color: var(--bg-surface-low);
|
||||||
box-shadow: var(--bs-surface-border);
|
box-shadow: var(--bs-surface-border);
|
||||||
border-radius: var(--bo-radius);
|
border-radius: var(--bo-radius);
|
||||||
|
padding: var(--sp-ultra-tight);
|
||||||
|
|
||||||
& > .ic-raw {
|
& > .ic-raw {
|
||||||
transform: scale(0.8);
|
transform: scale(0.8);
|
||||||
|
@ -38,6 +40,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__textarea-wrapper {
|
&__textarea-wrapper {
|
||||||
|
margin-left: var(--sp-extra-tight);
|
||||||
|
margin-right: var(--sp-extra-tight);
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -31,6 +31,7 @@ import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
||||||
|
|
||||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import { LoadingText } from '../../atoms/loading-text/LoadingText';
|
||||||
|
|
||||||
function SpaceManageBreadcrumb({ path, onSelect }) {
|
function SpaceManageBreadcrumb({ path, onSelect }) {
|
||||||
return (
|
return (
|
||||||
|
@ -289,19 +290,11 @@ function SpaceManageContent({ roomId, requestClose }) {
|
||||||
const [spacePath, addPathItem] = useSpacePath(roomId);
|
const [spacePath, addPathItem] = useSpacePath(roomId);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
const mountStore = useStore();
|
|
||||||
const currentPath = spacePath[spacePath.length - 1];
|
const currentPath = spacePath[spacePath.length - 1];
|
||||||
useChildUpdate(currentPath.roomId, roomsHierarchy);
|
useChildUpdate(currentPath.roomId, roomsHierarchy);
|
||||||
|
|
||||||
const currentHierarchy = roomsHierarchy.getHierarchy(currentPath.roomId);
|
const currentHierarchy = roomsHierarchy.getHierarchy(currentPath.roomId);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mountStore.setItem(true);
|
|
||||||
return () => {
|
|
||||||
mountStore.setItem(false);
|
|
||||||
};
|
|
||||||
}, [roomId]);
|
|
||||||
|
|
||||||
useEffect(() => setSelected([]), [spacePath]);
|
useEffect(() => setSelected([]), [spacePath]);
|
||||||
|
|
||||||
const handleSelected = (selectedRoomId) => {
|
const handleSelected = (selectedRoomId) => {
|
||||||
|
@ -323,11 +316,9 @@ function SpaceManageContent({ roomId, requestClose }) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await roomsHierarchy.load(currentPath.roomId);
|
await roomsHierarchy.load(currentPath.roomId);
|
||||||
if (!mountStore.getItem()) return;
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
} catch {
|
} catch(O_o) {
|
||||||
if (!mountStore.getItem()) return;
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
}
|
||||||
|
@ -362,7 +353,7 @@ function SpaceManageContent({ roomId, requestClose }) {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)))}
|
)))}
|
||||||
{!currentHierarchy && <Text>loading...</Text>}
|
{!currentHierarchy && <LoadingText />}
|
||||||
</div>
|
</div>
|
||||||
{currentHierarchy?.canLoadMore && !isLoading && (
|
{currentHierarchy?.canLoadMore && !isLoading && (
|
||||||
<Button onClick={loadRoomHierarchy}>Load more</Button>
|
<Button onClick={loadRoomHierarchy}>Load more</Button>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { isAuthenticated } from '../../client/state/auth';
|
import { isAuthenticated } from '../../client/state/auth';
|
||||||
|
import { LoadingText } from '../atoms/loading-text/LoadingText';
|
||||||
|
|
||||||
import Auth from '../templates/auth/Auth';
|
const Auth = React.lazy(() => import('../templates/auth/Auth'));
|
||||||
import Client from '../templates/client/Client';
|
const Client = React.lazy(() => import('../templates/client/Client'));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return isAuthenticated() ? <Client /> : <Auth />;
|
return isAuthenticated() ? <React.Suspense fallback={<LoadingText/>}><Client /></React.Suspense> : <React.Suspense fallback={<LoadingText/>}><Auth /></React.Suspense>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Auth.scss';
|
import './Auth.scss';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
|
|
||||||
import * as auth from '../../../client/action/auth';
|
import * as auth from '../../../client/action/auth';
|
||||||
|
@ -375,8 +374,7 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||||
const d = await auth.completeRegisterStage(baseUrl, username, password, { session });
|
const d = await auth.completeRegisterStage(baseUrl, username, password, { session });
|
||||||
|
|
||||||
if (isRecaptcha && !d.completed.includes('m.login.recaptcha')) {
|
if (isRecaptcha && !d.completed.includes('m.login.recaptcha')) {
|
||||||
const sitekey = params['m.login.recaptcha'].public_key;
|
setProcess({ isLoading: false, error: 'm.login.recaptcha is not supported.' });
|
||||||
setProcess({ type: 'm.login.recaptcha', sitekey });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isTerms && !d.completed.includes('m.login.terms')) {
|
if (isTerms && !d.completed.includes('m.login.terms')) {
|
||||||
|
@ -400,17 +398,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||||
asyncProcess();
|
asyncProcess();
|
||||||
}, [process]);
|
}, [process]);
|
||||||
|
|
||||||
const handleRecaptcha = async (value) => {
|
|
||||||
if (typeof value !== 'string') return;
|
|
||||||
const [username, password] = getInputs();
|
|
||||||
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
|
||||||
type: 'm.login.recaptcha',
|
|
||||||
response: value,
|
|
||||||
session,
|
|
||||||
});
|
|
||||||
if (d.done) refreshWindow();
|
|
||||||
else setProcess({ type: 'processing', message: 'Registration in progress...' });
|
|
||||||
};
|
|
||||||
const handleTerms = async () => {
|
const handleTerms = async () => {
|
||||||
const [username, password] = getInputs();
|
const [username, password] = getInputs();
|
||||||
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
||||||
|
@ -435,7 +422,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{process.type === 'processing' && <LoadingScreen message={process.message} />}
|
{process.type === 'processing' && <LoadingScreen message={process.message} />}
|
||||||
{process.type === 'm.login.recaptcha' && <Recaptcha message="Please check the box below to proceed." sitekey={process.sitekey} onChange={handleRecaptcha} />}
|
|
||||||
{process.type === 'm.login.terms' && <Terms url={process.url} onSubmit={handleTerms} />}
|
{process.type === 'm.login.terms' && <Terms url={process.url} onSubmit={handleTerms} />}
|
||||||
{process.type === 'm.login.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
|
{process.type === 'm.login.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
|
||||||
<div className="auth-form__heading">
|
<div className="auth-form__heading">
|
||||||
|
@ -607,22 +593,6 @@ LoadingScreen.propTypes = {
|
||||||
message: PropTypes.string.isRequired,
|
message: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Recaptcha({ message, sitekey, onChange }) {
|
|
||||||
return (
|
|
||||||
<ProcessWrapper>
|
|
||||||
<div style={{ marginBottom: 'var(--sp-normal)' }}>
|
|
||||||
<Text variant="s1" weight="medium">{message}</Text>
|
|
||||||
</div>
|
|
||||||
<ReCAPTCHA sitekey={sitekey} onChange={onChange} />
|
|
||||||
</ProcessWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Recaptcha.propTypes = {
|
|
||||||
message: PropTypes.string.isRequired,
|
|
||||||
sitekey: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Terms({ url, onSubmit }) {
|
function Terms({ url, onSubmit }) {
|
||||||
return (
|
return (
|
||||||
<ProcessWrapper>
|
<ProcessWrapper>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as sdk from 'matrix-js-sdk';
|
import { createClient } from 'matrix-js-sdk';
|
||||||
import cons from '../state/cons';
|
import cons from '../state/cons';
|
||||||
|
|
||||||
function updateLocalStore(accessToken, deviceId, userId, baseUrl) {
|
function updateLocalStore(accessToken, deviceId, userId, baseUrl) {
|
||||||
|
@ -9,7 +9,7 @@ function updateLocalStore(accessToken, deviceId, userId, baseUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTemporaryClient(baseUrl) {
|
function createTemporaryClient(baseUrl) {
|
||||||
return sdk.createClient({ baseUrl });
|
return createClient({ baseUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startSsoLogin(baseUrl, type, idpId) {
|
async function startSsoLogin(baseUrl, type, idpId) {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import * as sdk from 'matrix-js-sdk';
|
|
||||||
import Olm from '@matrix-org/olm';
|
import Olm from '@matrix-org/olm';
|
||||||
// import { logger } from 'matrix-js-sdk/lib/logger';
|
|
||||||
|
|
||||||
import { secret } from './state/auth';
|
import { secret } from './state/auth';
|
||||||
import RoomList from './state/RoomList';
|
import RoomList from './state/RoomList';
|
||||||
|
@ -10,11 +8,10 @@ import RoomsInput from './state/RoomsInput';
|
||||||
import Notifications from './state/Notifications';
|
import Notifications from './state/Notifications';
|
||||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||||
import navigation from './state/navigation';
|
import navigation from './state/navigation';
|
||||||
|
import { createClient, IndexedDBCryptoStore, IndexedDBStore } from 'matrix-js-sdk';
|
||||||
|
|
||||||
global.Olm = Olm;
|
global.Olm = Olm;
|
||||||
|
|
||||||
// logger.disableAll();
|
|
||||||
|
|
||||||
class InitMatrix extends EventEmitter {
|
class InitMatrix extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -29,19 +26,19 @@ class InitMatrix extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async startClient() {
|
async startClient() {
|
||||||
const indexedDBStore = new sdk.IndexedDBStore({
|
const indexedDBStore = new IndexedDBStore({
|
||||||
indexedDB: global.indexedDB,
|
indexedDB: global.indexedDB,
|
||||||
localStorage: global.localStorage,
|
localStorage: global.localStorage,
|
||||||
dbName: 'web-sync-store',
|
dbName: 'web-sync-store',
|
||||||
});
|
});
|
||||||
await indexedDBStore.startup();
|
await indexedDBStore.startup();
|
||||||
|
|
||||||
this.matrixClient = sdk.createClient({
|
this.matrixClient = createClient({
|
||||||
baseUrl: secret.baseUrl,
|
baseUrl: secret.baseUrl,
|
||||||
accessToken: secret.accessToken,
|
accessToken: secret.accessToken,
|
||||||
userId: secret.userId,
|
userId: secret.userId,
|
||||||
store: indexedDBStore,
|
store: indexedDBStore,
|
||||||
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||||
deviceId: secret.deviceId,
|
deviceId: secret.deviceId,
|
||||||
timelineSupport: true,
|
timelineSupport: true,
|
||||||
cryptoCallbacks,
|
cryptoCallbacks,
|
||||||
|
|
|
@ -40,7 +40,8 @@ class RoomsHierarchy {
|
||||||
try {
|
try {
|
||||||
await roomHierarchy.load(limit);
|
await roomHierarchy.load(limit);
|
||||||
return roomHierarchy.rooms;
|
return roomHierarchy.rooms;
|
||||||
} catch {
|
} catch (o_O) {
|
||||||
|
console.error(o_O);
|
||||||
return roomHierarchy.rooms;
|
return roomHierarchy.rooms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { StrictMode } from 'react';
|
||||||
import ReactDom from 'react-dom';
|
import ReactDom from 'react-dom';
|
||||||
import './font';
|
import './font';
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
@ -9,4 +9,9 @@ import App from './app/pages/App';
|
||||||
|
|
||||||
settings.applyTheme();
|
settings.applyTheme();
|
||||||
|
|
||||||
ReactDom.render(<App />, document.getElementById('root'));
|
ReactDom.render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|
|
@ -81,14 +81,15 @@
|
||||||
--ic-danger-normal: rgba(240, 71, 71, 0.7);
|
--ic-danger-normal: rgba(240, 71, 71, 0.7);
|
||||||
|
|
||||||
/* user mxid colors */
|
/* user mxid colors */
|
||||||
--mx-uc-1: hsl(208, 66%, 53%);
|
/* Thanks: Gruvbox Material Light */
|
||||||
--mx-uc-2: hsl(302, 49%, 45%);
|
--mx-uc-1: #af2528;
|
||||||
--mx-uc-3: hsl(163, 97%, 36%);
|
--mx-uc-2: #b94c07;
|
||||||
--mx-uc-4: hsl(343, 75%, 61%);
|
--mx-uc-3: #b4730e;
|
||||||
--mx-uc-5: hsl(24, 100%, 59%);
|
--mx-uc-4: #72761e;
|
||||||
--mx-uc-6: hsl(181, 63%, 47%);
|
--mx-uc-5: #477a5b;
|
||||||
--mx-uc-7: hsl(242, 89%, 65%);
|
--mx-uc-6: #266b79;
|
||||||
--mx-uc-8: hsl(94, 65%, 50%);
|
--mx-uc-7: #924f79;
|
||||||
|
--mx-uc-8: #af2528;
|
||||||
|
|
||||||
/* system icon size | -ic-[size]: value */
|
/* system icon size | -ic-[size]: value */
|
||||||
--ic-large: 38px;
|
--ic-large: 38px;
|
||||||
|
@ -194,6 +195,7 @@
|
||||||
|
|
||||||
--font-primary: 'Roboto', sans-serif;
|
--font-primary: 'Roboto', sans-serif;
|
||||||
--font-secondary: 'Roboto', sans-serif;
|
--font-secondary: 'Roboto', sans-serif;
|
||||||
|
--font-monospace: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -230,6 +232,16 @@
|
||||||
--bg-ping-hover: hsla(137deg, 100%, 38%, 50%);
|
--bg-ping-hover: hsla(137deg, 100%, 38%, 50%);
|
||||||
--bg-divider: hsla(0, 0%, 100%, .1);
|
--bg-divider: hsla(0, 0%, 100%, .1);
|
||||||
|
|
||||||
|
/* user mxid colors (dark theme) */
|
||||||
|
/* Thanks: Gruvbox Material Dark */
|
||||||
|
--mx-uc-1: #f2594b;
|
||||||
|
--mx-uc-2: #f28534;
|
||||||
|
--mx-uc-3: #e9b143;
|
||||||
|
--mx-uc-4: #b0b846;
|
||||||
|
--mx-uc-5: #8bba7f;
|
||||||
|
--mx-uc-6: #80aa9e;
|
||||||
|
--mx-uc-7: #d3869b;
|
||||||
|
--mx-uc-8: #f2594b;
|
||||||
|
|
||||||
/* text color | --tc-[background type]-[priority]: value */
|
/* text color | --tc-[background type]-[priority]: value */
|
||||||
--tc-surface-high: rgba(255, 255, 255, 98%);
|
--tc-surface-high: rgba(255, 255, 255, 98%);
|
||||||
|
@ -251,18 +263,6 @@
|
||||||
--ic-surface-low: rgba(255, 255, 255, 64%);
|
--ic-surface-low: rgba(255, 255, 255, 64%);
|
||||||
--ic-primary-normal: #ffffff;
|
--ic-primary-normal: #ffffff;
|
||||||
|
|
||||||
& .text {
|
|
||||||
/* override user mxid colors for texts */
|
|
||||||
--mx-uc-1: hsl(208, 100%, 58%);
|
|
||||||
--mx-uc-2: hsl(301, 80%, 70%);
|
|
||||||
--mx-uc-3: hsl(163, 93%, 41%);
|
|
||||||
--mx-uc-4: hsl(343, 91%, 66%);
|
|
||||||
--mx-uc-5: hsl(24, 90%, 67%);
|
|
||||||
--mx-uc-6: hsl(181, 90%, 50%);
|
|
||||||
--mx-uc-7: hsl(243, 100%, 74%);
|
|
||||||
--mx-uc-8: hsl(94, 66%, 50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* shadow and overlay */
|
/* shadow and overlay */
|
||||||
--bg-overlay: rgba(0, 0, 0, 60%);
|
--bg-overlay: rgba(0, 0, 0, 60%);
|
||||||
--bg-overlay-low: rgba(0, 0, 0, 80%);
|
--bg-overlay-low: rgba(0, 0, 0, 80%);
|
||||||
|
@ -331,6 +331,10 @@
|
||||||
--ic-surface-low: rgba(255, 251, 222, 64%);
|
--ic-surface-low: rgba(255, 251, 222, 64%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-monospace {
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
}
|
||||||
|
|
||||||
.font-primary {
|
.font-primary {
|
||||||
font-family: var(--font-primary);
|
font-family: var(--font-primary);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
/* eslint-disable import/prefer-default-export */
|
|
||||||
import React, { lazy, Suspense } from 'react';
|
|
||||||
|
|
||||||
import linkifyHtml from 'linkify-html';
|
import linkifyHtml from 'linkify-html';
|
||||||
import parse from 'html-react-parser';
|
import parse from 'html-react-parser';
|
||||||
import twemoji from 'twemoji';
|
import twemoji from 'twemoji';
|
||||||
|
@ -8,36 +5,44 @@ import { sanitizeText } from './sanitize';
|
||||||
|
|
||||||
export const TWEMOJI_BASE_URL = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/';
|
export const TWEMOJI_BASE_URL = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/';
|
||||||
|
|
||||||
const Math = lazy(() => import('../app/atoms/math/Math'));
|
// Start modified block from `twemoji` code:
|
||||||
|
// MIT License
|
||||||
|
// Copyright (c) 2021 Twitter
|
||||||
|
const UFE0Fg = /\uFE0F/g;
|
||||||
|
const U200D = String.fromCharCode(0x200D);
|
||||||
|
|
||||||
const mathOptions = {
|
function grabTheRightIcon(rawText) {
|
||||||
replace: (node) => {
|
// if variant is present as \uFE0F
|
||||||
const maths = node.attribs?.['data-mx-maths'];
|
return twemoji.convert.toCodePoint(rawText.indexOf(U200D) < 0 ?
|
||||||
if (maths) {
|
rawText.replace(UFE0Fg, '') :
|
||||||
return (
|
rawText
|
||||||
<Suspense fallback={<code>{maths}</code>}>
|
|
||||||
<Math
|
|
||||||
content={maths}
|
|
||||||
throwOnError={false}
|
|
||||||
errorColor="var(--tc-danger-normal)"
|
|
||||||
displayMode={node.name === 'div'}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
// End function from `twemoji`
|
||||||
},
|
|
||||||
};
|
export function singleEmojiToJSX({ shortcodes, unicode, hexcode }) {
|
||||||
|
return <img
|
||||||
|
className="emoji"
|
||||||
|
draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
alt={unicode}
|
||||||
|
unicode={unicode}
|
||||||
|
shortcodes={shortcodes}
|
||||||
|
hexcode={hexcode}
|
||||||
|
src={`${TWEMOJI_BASE_URL}/72x72/${grabTheRightIcon(unicode)}.png`}
|
||||||
|
></img>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} text - text to twemojify
|
* @param {string} text - text to twemojify
|
||||||
* @param {object|undefined} opts - options for tweomoji.parse
|
* @param {object|undefined} opts - options for tweomoji.parse
|
||||||
* @param {boolean} [linkify=false] - convert links to html tags (default: false)
|
* @param {boolean} [linkify=false] - convert links to html tags (default: false)
|
||||||
* @param {boolean} [sanitize=true] - sanitize html text (default: true)
|
* @param {boolean} [sanitize=true] - sanitize html text (default: true)
|
||||||
* @param {boolean} [maths=false] - render maths (default: false)
|
|
||||||
* @returns React component
|
* @returns React component
|
||||||
*/
|
*/
|
||||||
export function twemojify(text, opts, linkify = false, sanitize = true, maths = false) {
|
export function twemojify(text, opts, linkify = false, sanitize = true) {
|
||||||
if (typeof text !== 'string') return text;
|
if (typeof text !== 'string') return text;
|
||||||
let content = text;
|
let content = text;
|
||||||
const options = opts ?? { base: TWEMOJI_BASE_URL };
|
const options = opts ?? { base: TWEMOJI_BASE_URL };
|
||||||
|
@ -56,5 +61,5 @@ export function twemojify(text, opts, linkify = false, sanitize = true, maths =
|
||||||
rel: 'noreferrer noopener',
|
rel: 'noreferrer noopener',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return parse(content, maths ? mathOptions : null);
|
return parse(content, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import { wasm } from '@rollup/plugin-wasm';
|
import { wasm } from '@rollup/plugin-wasm';
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
|
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
|
||||||
import inject from '@rollup/plugin-inject';
|
import inject from '@rollup/plugin-inject';
|
||||||
import { svgLoader } from './viteSvgLoader';
|
import { svgLoader } from './viteSvgLoader';
|
||||||
|
import { preact } from '@preact/preset-vite';
|
||||||
|
|
||||||
const copyFiles = {
|
const copyFiles = {
|
||||||
targets: [
|
targets: [
|
||||||
|
@ -39,7 +39,7 @@ export default defineConfig({
|
||||||
viteStaticCopy(copyFiles),
|
viteStaticCopy(copyFiles),
|
||||||
svgLoader(),
|
svgLoader(),
|
||||||
wasm(),
|
wasm(),
|
||||||
react(),
|
preact(),
|
||||||
],
|
],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
esbuildOptions: {
|
esbuildOptions: {
|
||||||
|
@ -62,7 +62,26 @@ export default defineConfig({
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
plugins: [
|
plugins: [
|
||||||
inject({ Buffer: ['buffer', 'Buffer'] })
|
inject({ Buffer: ['buffer', 'Buffer'] })
|
||||||
]
|
],
|
||||||
|
output: {
|
||||||
|
manualChunks: (id) => {
|
||||||
|
if (id.includes("node_modules")) {
|
||||||
|
if (id.includes("matrix")) {
|
||||||
|
return "vendor-matrix";
|
||||||
|
}
|
||||||
|
if (id.includes("emojibase")) {
|
||||||
|
return "vendor-emojibase";
|
||||||
|
}
|
||||||
|
return "vendor";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
treeshake: "smallest"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"react/jsx-runtime.js": "preact/compat/jsx-runtime"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue