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>
|
||||
</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)
|
||||
- [Contributing](./CONTRIBUTING.md)
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"defaultHomeserver": 3,
|
||||
"defaultHomeserver": 0,
|
||||
"homeserverList": [
|
||||
"http://localhost:3000",
|
||||
"converser.eu",
|
||||
"envs.net",
|
||||
"halogen.city",
|
||||
|
|
144
index.html
144
index.html
|
@ -1,101 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cinny</title>
|
||||
<meta name="name" content="Cinny" />
|
||||
<meta name="author" content="Ajay Bura" />
|
||||
<meta
|
||||
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."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element"
|
||||
/>
|
||||
|
||||
<meta property="og:title" content="Cinny" />
|
||||
<meta property="og:url" content="https://cinny.in" />
|
||||
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png" />
|
||||
<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" />
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Cinny</title>
|
||||
<meta name="name" content="Cinny" />
|
||||
<meta name="author" content="Ajay Bura" />
|
||||
<meta 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." />
|
||||
<meta name="keywords" content="cinny, cinnyapp, cinnychat, matrix, matrix client, matrix.org, element" />
|
||||
|
||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||
<meta property="og:title" content="Cinny" />
|
||||
<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" />
|
||||
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="application-name" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="57x57"
|
||||
href="./public/res/apple/apple-touch-icon-57x57.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="60x60"
|
||||
href="./public/res/apple/apple-touch-icon-60x60.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>
|
||||
<body id="appBody">
|
||||
<script>
|
||||
window.global ||= window;
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<audio id="notificationSound">
|
||||
<source src="./public/sound/notification.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<audio id="inviteSound">
|
||||
<source src="./public/sound/invite.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<script type="module" src="./src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="application-name" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-title" content="Cinny" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./public/res/apple/apple-touch-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="./public/res/apple/apple-touch-icon-60x60.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>
|
||||
|
||||
<body id="appBody">
|
||||
<script>
|
||||
window.global ||= window;
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<audio id="notificationSound">
|
||||
<source src="./public/sound/notification.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<audio id="inviteSound">
|
||||
<source src="./public/sound/invite.ogg" type="audio/ogg" />
|
||||
</audio>
|
||||
<script type="module" src="./src/index.jsx"></script>
|
||||
</body>
|
||||
|
||||
</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": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "yarn check:eslint && yarn check:prettier",
|
||||
"check:eslint": "eslint src/*",
|
||||
"check:prettier": "prettier --check .",
|
||||
|
@ -19,59 +20,54 @@
|
|||
"author": "Ajay Bura",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "4.5.14",
|
||||
"@fontsource/inter": "4.5.15",
|
||||
"@fontsource/roboto": "4.5.8",
|
||||
"@khanacademy/simple-markdown": "0.8.6",
|
||||
"@matrix-org/olm": "3.2.14",
|
||||
"@tippyjs/react": "4.2.6",
|
||||
"blurhash": "2.0.4",
|
||||
"blurhash": "2.0.5",
|
||||
"browser-encrypt-attachment": "0.3.0",
|
||||
"dateformat": "5.0.3",
|
||||
"emojibase-data": "7.0.1",
|
||||
"file-saver": "2.0.5",
|
||||
"flux": "4.0.3",
|
||||
"flux": "4.0.4",
|
||||
"formik": "2.2.9",
|
||||
"html-react-parser": "3.0.4",
|
||||
"katex": "0.16.4",
|
||||
"linkify-html": "4.0.2",
|
||||
"linkifyjs": "4.0.2",
|
||||
"matrix-js-sdk": "22.0.0",
|
||||
"html-react-parser": "3.0.13",
|
||||
"linkify-html": "4.1.0",
|
||||
"linkifyjs": "4.1.0",
|
||||
"matrix-js-sdk": "23.5.0",
|
||||
"preact": "10.13.1",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
"react-autosize-textarea": "7.1.0",
|
||||
"react-blurhash": "0.2.0",
|
||||
"react-dnd": "15.1.2",
|
||||
"react-dnd-html5-backend": "15.1.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-blurhash": "0.3.0",
|
||||
"react-dnd": "16.0.1",
|
||||
"react-dnd-html5-backend": "16.0.1",
|
||||
"react-modal": "3.16.1",
|
||||
"sanitize-html": "2.8.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"tippy.js": "6.3.7",
|
||||
"twemoji": "14.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||
"@preact/preset-vite": "2.5.0",
|
||||
"@rollup/plugin-inject": "5.0.3",
|
||||
"@rollup/plugin-wasm": "6.1.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"@vitejs/plugin-react": "3.0.0",
|
||||
"@rollup/plugin-wasm": "6.1.2",
|
||||
"@types/node": "18.15.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.56.0",
|
||||
"@typescript-eslint/parser": "5.56.0",
|
||||
"buffer": "6.0.3",
|
||||
"eslint": "8.29.0",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "6.6.1",
|
||||
"eslint-plugin-react": "7.31.11",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"mini-svg-data-uri": "1.4.4",
|
||||
"prettier": "2.8.1",
|
||||
"sass": "1.56.2",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "4.0.1",
|
||||
"vite-plugin-static-copy": "0.13.0"
|
||||
"prettier": "2.8.6",
|
||||
"sass": "1.59.3",
|
||||
"typescript": "5.0.2",
|
||||
"vite": "4.2.1",
|
||||
"vite-plugin-static-copy": "0.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ const Avatar = React.forwardRef(({
|
|||
iconSrc !== null
|
||||
? <RawIcon size={size} src={iconSrc} color={iconColor} />
|
||||
: text !== null && (
|
||||
<Text variant={textSize} primary>
|
||||
<Text variant={textSize} monospace>
|
||||
{twemojify(avatarInitials(text))}
|
||||
</Text>
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
display: inline-flex;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: var(--bo-radius);
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
&__large {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './ContextMenu.scss';
|
||||
|
||||
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 Button from '../button/Button';
|
||||
|
@ -13,8 +13,12 @@ function ContextMenu({
|
|||
content, placement, maxWidth, render, afterToggle,
|
||||
}) {
|
||||
const [isVisible, setVisibility] = useState(false);
|
||||
const showMenu = () => setVisibility(true);
|
||||
const hideMenu = () => setVisibility(false);
|
||||
const showMenu = useCallback(() => {
|
||||
setVisibility(true);
|
||||
});
|
||||
const hideMenu = useCallback(() => {
|
||||
setVisibility(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (afterToggle !== null) afterToggle(isVisible);
|
||||
|
@ -22,7 +26,7 @@ function ContextMenu({
|
|||
|
||||
return (
|
||||
<Tippy
|
||||
animation="scale-extreme"
|
||||
animation="scale-subtle"
|
||||
className="context-menu"
|
||||
visible={isVisible}
|
||||
onClickOutside={hideMenu}
|
||||
|
@ -31,7 +35,7 @@ function ContextMenu({
|
|||
interactive
|
||||
arrow={false}
|
||||
maxWidth={maxWidth}
|
||||
duration={200}
|
||||
duration={150}
|
||||
>
|
||||
{render(isVisible ? hideMenu : showMenu)}
|
||||
</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 {
|
||||
animation: raw-modal--overlay 150ms;
|
||||
animation: raw-modal--overlay 210ms;
|
||||
animation-timing-function: cubic-bezier(0.77,0,0.18,1);
|
||||
}
|
||||
|
||||
.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 {
|
||||
0% {
|
||||
transform: translateY(100px);
|
||||
opacity: .5;
|
||||
transform: scale(0.90);
|
||||
opacity: .4;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,10 +41,10 @@
|
|||
}
|
||||
|
||||
@mixin scroll__h {
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@mixin scroll__v {
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@mixin scroll--auto-hide {
|
||||
@extend .firefox-scrollbar--transparent;
|
||||
|
|
|
@ -4,13 +4,14 @@ import './Text.scss';
|
|||
|
||||
function Text({
|
||||
className, style, variant, weight,
|
||||
primary, span, children,
|
||||
primary, monospace, span, children,
|
||||
}) {
|
||||
const classes = [];
|
||||
if (className) classes.push(className);
|
||||
|
||||
classes.push(`text text-${variant} text-${weight}`);
|
||||
if (primary) classes.push('font-primary');
|
||||
if (monospace) classes.push('font-monospace');
|
||||
|
||||
const textClass = classes.join(' ');
|
||||
if (span) return <span className={textClass} style={style}>{ children }</span>;
|
||||
|
@ -26,6 +27,7 @@ Text.defaultProps = {
|
|||
variant: 'b1',
|
||||
weight: 'normal',
|
||||
primary: false,
|
||||
monospace: false,
|
||||
span: false,
|
||||
};
|
||||
|
||||
|
@ -35,6 +37,7 @@ Text.propTypes = {
|
|||
variant: PropTypes.oneOf(['h1', 'h2', 's1', 'b1', 'b2', 'b3']),
|
||||
weight: PropTypes.oneOf(['light', 'normal', 'medium', 'bold']),
|
||||
primary: PropTypes.bool,
|
||||
monospace: PropTypes.bool,
|
||||
span: PropTypes.bool,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
content-visibility: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
|
@ -6,29 +6,24 @@ import { isInSameDay } from '../../../util/common';
|
|||
|
||||
function Time({ timestamp, fullTime }) {
|
||||
const date = new Date(timestamp);
|
||||
let formattedDate;
|
||||
|
||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
||||
let formattedDate = formattedFullTime;
|
||||
|
||||
if (!fullTime) {
|
||||
if (fullTime) {
|
||||
formattedDate = formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT')
|
||||
} else {
|
||||
const compareDate = new Date();
|
||||
const isToday = isInSameDay(date, compareDate);
|
||||
compareDate.setDate(compareDate.getDate() - 1);
|
||||
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) {
|
||||
formattedDate = `Yesterday, ${formattedDate}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
title={formattedFullTime}
|
||||
>
|
||||
{formattedDate}
|
||||
</time>
|
||||
<time dateTime={date.toISOString()}>{formattedDate}</time>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import './Tooltip.scss';
|
|||
import Tippy from '@tippyjs/react';
|
||||
|
||||
function Tooltip({
|
||||
className, placement, content, delay, children,
|
||||
className, placement, content, children,
|
||||
}) {
|
||||
return (
|
||||
<Tippy
|
||||
|
@ -14,8 +14,8 @@ function Tooltip({
|
|||
arrow={false}
|
||||
maxWidth={250}
|
||||
placement={placement}
|
||||
delay={delay}
|
||||
duration={[100, 0]}
|
||||
duration={[75, 0]}
|
||||
animation="scale-subtle"
|
||||
>
|
||||
{children}
|
||||
</Tippy>
|
||||
|
@ -25,14 +25,12 @@ function Tooltip({
|
|||
Tooltip.defaultProps = {
|
||||
placement: 'top',
|
||||
className: '',
|
||||
delay: [200, 0],
|
||||
};
|
||||
|
||||
Tooltip.propTypes = {
|
||||
className: PropTypes.string,
|
||||
placement: PropTypes.string,
|
||||
content: PropTypes.node.isRequired,
|
||||
delay: PropTypes.arrayOf(PropTypes.number),
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -85,11 +85,9 @@ const MessageHeader = React.memo(({
|
|||
<span>{twemojify(username)}</span>
|
||||
<span>{twemojify(userId)}</span>
|
||||
</Text>
|
||||
<div className="message__time">
|
||||
<Text variant="b3">
|
||||
<Time timestamp={timestamp} fullTime={fullTime} />
|
||||
</Text>
|
||||
</div>
|
||||
<Text className="message__time" variant="b3">
|
||||
<Time timestamp={timestamp} fullTime={fullTime} />
|
||||
</Text>
|
||||
</div>
|
||||
));
|
||||
MessageHeader.defaultProps = {
|
||||
|
@ -233,7 +231,7 @@ const MessageBody = React.memo(({
|
|||
if (content.type === 'img') {
|
||||
// If this messages contains only a single (inline) image
|
||||
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
|
||||
|
||||
// Count the number of emojis
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
& .message__profile {
|
||||
min-width: 0;
|
||||
color: var(--tc-surface-high);
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
@include dir.side(margin, 0, var(--sp-ultra-tight));
|
||||
|
||||
& > span {
|
||||
@extend .cp-txt__ellipsis;
|
||||
|
@ -128,13 +128,8 @@
|
|||
}
|
||||
|
||||
& .message__time {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
& > .text {
|
||||
white-space: nowrap;
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
white-space: nowrap;
|
||||
color: var(--tc-surface-low);
|
||||
}
|
||||
}
|
||||
.message__reply {
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
@use '../../partials/text';
|
||||
|
||||
.people-selector {
|
||||
width: 100%;
|
||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
margin: var(--sp-extra-tight);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
& .segmented-controls {
|
||||
@include dir.side(margin, 0, var(--sp-normal));
|
||||
margin: var(--sp-extra-tight);
|
||||
& > button {
|
||||
padding: var(--sp-ultra-tight) 0;
|
||||
}
|
||||
|
|
|
@ -177,6 +177,11 @@ function RoomPermissions({ roomId }) {
|
|||
const mx = initMatrix.matrixClient;
|
||||
const room = mx.getRoom(roomId);
|
||||
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
|
||||
|
||||
if (!pLEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const permissions = pLEvent.getContent();
|
||||
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
|
||||
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
|
||||
|
|
|
@ -2,16 +2,15 @@
|
|||
@use '../../partials/dir';
|
||||
|
||||
.room-profile {
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__row;
|
||||
align-items: center;
|
||||
& .avatar-container {
|
||||
min-width: var(--av-large);
|
||||
}
|
||||
}
|
||||
|
||||
&__display {
|
||||
align-self: flex-end;
|
||||
@include dir.side(margin, var(--sp-loose), 0);
|
||||
|
||||
& > div:first-child {
|
||||
|
|
|
@ -54,9 +54,6 @@
|
|||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
& .message__time {
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import './EmojiBoard.scss';
|
||||
|
||||
import parse from 'html-react-parser';
|
||||
import twemoji from 'twemoji';
|
||||
import parse from "html-react-parser";
|
||||
import { emojiGroups, emojis } from './emoji';
|
||||
import { getRelevantPacks } from './custom-emoji';
|
||||
import initMatrix from '../../../client/initMatrix';
|
||||
|
@ -13,7 +12,6 @@ import cons from '../../../client/state/cons';
|
|||
import navigation from '../../../client/state/navigation';
|
||||
import AsyncSearch from '../../../util/AsyncSearch';
|
||||
import { addRecentEmoji, getRecentEmojis } from './recent';
|
||||
import { TWEMOJI_BASE_URL } from '../../../util/twemojify';
|
||||
|
||||
import Text from '../../atoms/text/Text';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
@ -49,17 +47,7 @@ const EmojiGroup = React.memo(({ name, groupEmojis }) => {
|
|||
<span key={emojiIndex}>
|
||||
{emoji.hexcode ? (
|
||||
// This is a unicode emoji, and should be rendered with twemoji
|
||||
parse(
|
||||
twemoji.parse(emoji.unicode, {
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
hexcode: emoji.hexcode,
|
||||
loading: 'lazy',
|
||||
}),
|
||||
base: TWEMOJI_BASE_URL,
|
||||
})
|
||||
)
|
||||
<span className="emoji" unicode={emoji.unicode} shortcodes={emoji.shortcode} data-mx-emoticon={emoji.mxc}>{ emoji.unicode }</span>
|
||||
) : (
|
||||
// This is a custom emoji, and should be render as an mxc
|
||||
<img
|
||||
|
@ -143,7 +131,7 @@ function SearchedEmoji() {
|
|||
|
||||
function EmojiBoard({ onSelect, searchRef }) {
|
||||
const scrollEmojisRef = useRef(null);
|
||||
const emojiInfo = useRef(null);
|
||||
const [emojiInfo, setEmojiInfo] = useState(null);
|
||||
|
||||
function isTargetNotEmoji(target) {
|
||||
return target.classList.contains('emoji') === false;
|
||||
|
@ -171,34 +159,15 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||
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) {
|
||||
if (isTargetNotEmoji(e.target)) return;
|
||||
|
||||
const emoji = e.target;
|
||||
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;
|
||||
searchRef.current.setAttribute('placeholder', shortcodes[0]);
|
||||
setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
|
||||
setEmojiInfo({ shortcode: shortcodes[0], unicode });
|
||||
}
|
||||
|
||||
function handleSearchChange() {
|
||||
|
@ -339,9 +308,9 @@ function EmojiBoard({ onSelect, searchRef }) {
|
|||
</div>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div ref={emojiInfo} className="emoji-board__content__info">
|
||||
<div>{parse(twemoji.parse('🙂', { base: TWEMOJI_BASE_URL }))}</div>
|
||||
<Text>:slight_smile:</Text>
|
||||
<div className="emoji-board__content__info">
|
||||
<div><span className="emoji">{ emojiInfo ? emojiInfo.unicode : '' }</span></div>
|
||||
<Text>{emojiInfo ? emojiInfo.shortcode : ''}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
.emoji-board {
|
||||
--emoji-board-height: 390px;
|
||||
--emoji-board-width: 286px;
|
||||
--emoji-board-width: 218px;
|
||||
display: flex;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
|
@ -121,6 +121,7 @@
|
|||
@include dir.side(margin, var(--left-margin), var(--right-margin));
|
||||
}
|
||||
& .emoji {
|
||||
display: block;
|
||||
max-width: 38px;
|
||||
max-height: 38px;
|
||||
width: 100%;
|
||||
|
|
|
@ -132,13 +132,13 @@ function DrawerHeader({ selectedTab, spaceId }) {
|
|||
onMouseUp={(e) => blurOnBubbling(e, '.drawer-header__btn')}
|
||||
>
|
||||
<TitleWrapper>
|
||||
<Text variant="s1" weight="medium" primary>{twemojify(spaceName)}</Text>
|
||||
<Text primary>{twemojify(spaceName)}</Text>
|
||||
</TitleWrapper>
|
||||
<RawIcon size="small" src={ChevronBottomIC} />
|
||||
</button>
|
||||
) : (
|
||||
<TitleWrapper>
|
||||
<Text variant="s1" weight="medium" primary>{tabName}</Text>
|
||||
<Text primary>{tabName}</Text>
|
||||
</TitleWrapper>
|
||||
)}
|
||||
|
||||
|
|
|
@ -48,7 +48,8 @@
|
|||
}
|
||||
|
||||
& .room-selector {
|
||||
width: calc(100% - var(--sp-extra-tight));
|
||||
@include dir.side(margin, auto, 0);
|
||||
margin: var(--sp-extra-tight);
|
||||
margin-top: var(--sp-ultra-tight);
|
||||
margin-bottom: var(--sp-ultra-tight);
|
||||
}
|
||||
}
|
|
@ -62,7 +62,7 @@
|
|||
.sidebar__cross-signin-alert .avatar-container {
|
||||
box-shadow: var(--bs-danger-border);
|
||||
animation-name: pushRight;
|
||||
animation-duration: 400ms;
|
||||
animation-duration: 250ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
.profile-viewer {
|
||||
&__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: var(--sp-normal);
|
||||
|
||||
&__info {
|
||||
align-self: flex-end;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ function PeopleDrawer({ roomId }) {
|
|||
<div className="people-drawer">
|
||||
<Header>
|
||||
<TitleWrapper>
|
||||
<Text variant="s1" primary>
|
||||
<Text span primary>
|
||||
People
|
||||
<Text className="people-drawer__member-count" variant="b3">{`${room.getJoinedMemberCount()} members`}</Text>
|
||||
</Text>
|
||||
|
|
|
@ -68,15 +68,13 @@
|
|||
& .people-selector {
|
||||
padding: var(--sp-extra-tight);
|
||||
border-radius: var(--bo-radius);
|
||||
&__container {
|
||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||
}
|
||||
}
|
||||
|
||||
& .segmented-controls {
|
||||
display: flex;
|
||||
margin-bottom: var(--sp-extra-tight);
|
||||
@include dir.side(margin, var(--sp-extra-tight), 0);
|
||||
margin: var(--sp-extra-tight);
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--sp-tight);
|
||||
}
|
||||
& .segment-btn {
|
||||
flex: 1;
|
||||
|
|
|
@ -40,6 +40,5 @@
|
|||
min-height: 85px;
|
||||
position: relative;
|
||||
background: var(--bg-surface);
|
||||
border-top: 1px solid var(--bg-surface-border);
|
||||
}
|
||||
}
|
|
@ -2,10 +2,8 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 { getEmojiForCompletion } from '../emoji-board/custom-emoji';
|
||||
|
@ -53,15 +51,7 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
|||
|
||||
// Renders a small Twemoji
|
||||
function renderTwemoji(emoji) {
|
||||
return parse(
|
||||
twemoji.parse(emoji.unicode, {
|
||||
attributes: () => ({
|
||||
unicode: emoji.unicode,
|
||||
shortcodes: emoji.shortcodes?.toString(),
|
||||
}),
|
||||
base: TWEMOJI_BASE_URL,
|
||||
})
|
||||
);
|
||||
return singleEmojiToJSX(emoji);
|
||||
}
|
||||
|
||||
// Render a custom emoji
|
||||
|
@ -69,6 +59,9 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
|
|||
return (
|
||||
<img
|
||||
className="emoji"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
src={mx.mxcUrlToHttp(emoji.mxc)}
|
||||
data-mx-emoticon=""
|
||||
alt={`:${emoji.shortcode}:`}
|
||||
|
|
|
@ -90,9 +90,9 @@ function RoomViewHeader({ roomId }) {
|
|||
>
|
||||
<Avatar imageSrc={avatarSrc} text={roomName} bgColor={colorMXID(roomId)} size="small" />
|
||||
<TitleWrapper>
|
||||
<Text variant="h2" weight="medium" primary>{twemojify(roomName)}</Text>
|
||||
<Text weight="bold" primary>{twemojify(roomName)}</Text>
|
||||
</TitleWrapper>
|
||||
<RawIcon src={ChevronBottomIC} />
|
||||
<RawIcon size="extra-small" src={ChevronBottomIC} />
|
||||
</button>
|
||||
{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} />
|
||||
|
|
|
@ -361,12 +361,12 @@ function RoomViewInput({
|
|||
}
|
||||
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">
|
||||
{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>
|
||||
<Text className="room-input__textarea-wrapper">
|
||||
<TextareaAutosize
|
||||
|
@ -380,42 +380,43 @@ function RoomViewInput({
|
|||
/>
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</div>
|
||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
openReusableContextMenu(
|
||||
'top',
|
||||
(() => {
|
||||
const cords = getEventCords(e);
|
||||
cords.y -= 20;
|
||||
return cords;
|
||||
})(),
|
||||
(closeMenu) => (
|
||||
<StickerBoard
|
||||
roomId={roomId}
|
||||
onSelect={(data) => {
|
||||
handleSendSticker(data);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}}
|
||||
tooltip="Sticker"
|
||||
src={StickerIC}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
const cords = getEventCords(e);
|
||||
cords.x += (document.dir === 'rtl' ? -80 : 80);
|
||||
cords.y -= 250;
|
||||
openEmojiBoard(cords, addEmoji);
|
||||
}}
|
||||
tooltip="Emoji"
|
||||
src={EmojiIC}
|
||||
/>
|
||||
<IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
|
||||
<div ref={rightOptionsRef} className="room-input__option-container">
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
openReusableContextMenu(
|
||||
'top',
|
||||
(() => {
|
||||
const cords = getEventCords(e);
|
||||
cords.y -= 20;
|
||||
return cords;
|
||||
})(),
|
||||
(closeMenu) => (
|
||||
<StickerBoard
|
||||
roomId={roomId}
|
||||
onSelect={(data) => {
|
||||
handleSendSticker(data);
|
||||
closeMenu();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}}
|
||||
tooltip="Sticker"
|
||||
src={StickerIC}
|
||||
size="small"
|
||||
/>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
const cords = getEventCords(e);
|
||||
cords.x += (document.dir === 'rtl' ? -80 : 80);
|
||||
cords.y -= 250;
|
||||
openEmojiBoard(cords, addEmoji);
|
||||
}}
|
||||
tooltip="Emoji"
|
||||
src={EmojiIC}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
@use '../../partials/dir';
|
||||
|
||||
.room-input {
|
||||
padding: var(--sp-extra-tight) calc(var(--sp-normal) - 2px);
|
||||
display: flex;
|
||||
min-height: 56px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__alert {
|
||||
margin: auto;
|
||||
|
@ -21,6 +22,7 @@
|
|||
background-color: var(--bg-surface-low);
|
||||
box-shadow: var(--bs-surface-border);
|
||||
border-radius: var(--bo-radius);
|
||||
padding: var(--sp-ultra-tight);
|
||||
|
||||
& > .ic-raw {
|
||||
transform: scale(0.8);
|
||||
|
@ -38,6 +40,8 @@
|
|||
}
|
||||
|
||||
&__textarea-wrapper {
|
||||
margin-left: var(--sp-extra-tight);
|
||||
margin-right: var(--sp-extra-tight);
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -31,6 +31,7 @@ import InfoIC from '../../../../public/res/ic/outlined/info.svg';
|
|||
|
||||
import { useForceUpdate } from '../../hooks/useForceUpdate';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { LoadingText } from '../../atoms/loading-text/LoadingText';
|
||||
|
||||
function SpaceManageBreadcrumb({ path, onSelect }) {
|
||||
return (
|
||||
|
@ -289,19 +290,11 @@ function SpaceManageContent({ roomId, requestClose }) {
|
|||
const [spacePath, addPathItem] = useSpacePath(roomId);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const mountStore = useStore();
|
||||
const currentPath = spacePath[spacePath.length - 1];
|
||||
useChildUpdate(currentPath.roomId, roomsHierarchy);
|
||||
|
||||
const currentHierarchy = roomsHierarchy.getHierarchy(currentPath.roomId);
|
||||
|
||||
useEffect(() => {
|
||||
mountStore.setItem(true);
|
||||
return () => {
|
||||
mountStore.setItem(false);
|
||||
};
|
||||
}, [roomId]);
|
||||
|
||||
useEffect(() => setSelected([]), [spacePath]);
|
||||
|
||||
const handleSelected = (selectedRoomId) => {
|
||||
|
@ -323,11 +316,9 @@ function SpaceManageContent({ roomId, requestClose }) {
|
|||
setIsLoading(true);
|
||||
try {
|
||||
await roomsHierarchy.load(currentPath.roomId);
|
||||
if (!mountStore.getItem()) return;
|
||||
setIsLoading(false);
|
||||
forceUpdate();
|
||||
} catch {
|
||||
if (!mountStore.getItem()) return;
|
||||
} catch(O_o) {
|
||||
setIsLoading(false);
|
||||
forceUpdate();
|
||||
}
|
||||
|
@ -362,7 +353,7 @@ function SpaceManageContent({ roomId, requestClose }) {
|
|||
/>
|
||||
)
|
||||
)))}
|
||||
{!currentHierarchy && <Text>loading...</Text>}
|
||||
{!currentHierarchy && <LoadingText />}
|
||||
</div>
|
||||
{currentHierarchy?.canLoadMore && !isLoading && (
|
||||
<Button onClick={loadRoomHierarchy}>Load more</Button>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
|
||||
import { isAuthenticated } from '../../client/state/auth';
|
||||
import { LoadingText } from '../atoms/loading-text/LoadingText';
|
||||
|
||||
import Auth from '../templates/auth/Auth';
|
||||
import Client from '../templates/client/Client';
|
||||
const Auth = React.lazy(() => import('../templates/auth/Auth'));
|
||||
const Client = React.lazy(() => import('../templates/client/Client'));
|
||||
|
||||
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;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './Auth.scss';
|
||||
import ReCAPTCHA from 'react-google-recaptcha';
|
||||
import { Formik } from 'formik';
|
||||
|
||||
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 });
|
||||
|
||||
if (isRecaptcha && !d.completed.includes('m.login.recaptcha')) {
|
||||
const sitekey = params['m.login.recaptcha'].public_key;
|
||||
setProcess({ type: 'm.login.recaptcha', sitekey });
|
||||
setProcess({ isLoading: false, error: 'm.login.recaptcha is not supported.' });
|
||||
return;
|
||||
}
|
||||
if (isTerms && !d.completed.includes('m.login.terms')) {
|
||||
|
@ -400,17 +398,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
|||
asyncProcess();
|
||||
}, [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 [username, password] = getInputs();
|
||||
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
||||
|
@ -435,7 +422,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
|||
return (
|
||||
<>
|
||||
{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.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
|
||||
<div className="auth-form__heading">
|
||||
|
@ -607,22 +593,6 @@ LoadingScreen.propTypes = {
|
|||
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 }) {
|
||||
return (
|
||||
<ProcessWrapper>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as sdk from 'matrix-js-sdk';
|
||||
import { createClient } from 'matrix-js-sdk';
|
||||
import cons from '../state/cons';
|
||||
|
||||
function updateLocalStore(accessToken, deviceId, userId, baseUrl) {
|
||||
|
@ -9,7 +9,7 @@ function updateLocalStore(accessToken, deviceId, userId, baseUrl) {
|
|||
}
|
||||
|
||||
function createTemporaryClient(baseUrl) {
|
||||
return sdk.createClient({ baseUrl });
|
||||
return createClient({ baseUrl });
|
||||
}
|
||||
|
||||
async function startSsoLogin(baseUrl, type, idpId) {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import EventEmitter from 'events';
|
||||
import * as sdk from 'matrix-js-sdk';
|
||||
import Olm from '@matrix-org/olm';
|
||||
// import { logger } from 'matrix-js-sdk/lib/logger';
|
||||
|
||||
import { secret } from './state/auth';
|
||||
import RoomList from './state/RoomList';
|
||||
|
@ -10,11 +8,10 @@ import RoomsInput from './state/RoomsInput';
|
|||
import Notifications from './state/Notifications';
|
||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||
import navigation from './state/navigation';
|
||||
import { createClient, IndexedDBCryptoStore, IndexedDBStore } from 'matrix-js-sdk';
|
||||
|
||||
global.Olm = Olm;
|
||||
|
||||
// logger.disableAll();
|
||||
|
||||
class InitMatrix extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
@ -29,19 +26,19 @@ class InitMatrix extends EventEmitter {
|
|||
}
|
||||
|
||||
async startClient() {
|
||||
const indexedDBStore = new sdk.IndexedDBStore({
|
||||
const indexedDBStore = new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
localStorage: global.localStorage,
|
||||
dbName: 'web-sync-store',
|
||||
});
|
||||
await indexedDBStore.startup();
|
||||
|
||||
this.matrixClient = sdk.createClient({
|
||||
this.matrixClient = createClient({
|
||||
baseUrl: secret.baseUrl,
|
||||
accessToken: secret.accessToken,
|
||||
userId: secret.userId,
|
||||
store: indexedDBStore,
|
||||
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||
deviceId: secret.deviceId,
|
||||
timelineSupport: true,
|
||||
cryptoCallbacks,
|
||||
|
|
|
@ -40,7 +40,8 @@ class RoomsHierarchy {
|
|||
try {
|
||||
await roomHierarchy.load(limit);
|
||||
return roomHierarchy.rooms;
|
||||
} catch {
|
||||
} catch (o_O) {
|
||||
console.error(o_O);
|
||||
return roomHierarchy.rooms;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { StrictMode } from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
import './font';
|
||||
import './index.scss';
|
||||
|
@ -9,4 +9,9 @@ import App from './app/pages/App';
|
|||
|
||||
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);
|
||||
|
||||
/* user mxid colors */
|
||||
--mx-uc-1: hsl(208, 66%, 53%);
|
||||
--mx-uc-2: hsl(302, 49%, 45%);
|
||||
--mx-uc-3: hsl(163, 97%, 36%);
|
||||
--mx-uc-4: hsl(343, 75%, 61%);
|
||||
--mx-uc-5: hsl(24, 100%, 59%);
|
||||
--mx-uc-6: hsl(181, 63%, 47%);
|
||||
--mx-uc-7: hsl(242, 89%, 65%);
|
||||
--mx-uc-8: hsl(94, 65%, 50%);
|
||||
/* Thanks: Gruvbox Material Light */
|
||||
--mx-uc-1: #af2528;
|
||||
--mx-uc-2: #b94c07;
|
||||
--mx-uc-3: #b4730e;
|
||||
--mx-uc-4: #72761e;
|
||||
--mx-uc-5: #477a5b;
|
||||
--mx-uc-6: #266b79;
|
||||
--mx-uc-7: #924f79;
|
||||
--mx-uc-8: #af2528;
|
||||
|
||||
/* system icon size | -ic-[size]: value */
|
||||
--ic-large: 38px;
|
||||
|
@ -194,6 +195,7 @@
|
|||
|
||||
--font-primary: 'Roboto', sans-serif;
|
||||
--font-secondary: 'Roboto', sans-serif;
|
||||
--font-monospace: monospace;
|
||||
}
|
||||
|
||||
|
||||
|
@ -230,6 +232,16 @@
|
|||
--bg-ping-hover: hsla(137deg, 100%, 38%, 50%);
|
||||
--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 */
|
||||
--tc-surface-high: rgba(255, 255, 255, 98%);
|
||||
|
@ -250,18 +262,6 @@
|
|||
--ic-surface-normal: rgba(255, 255, 255, 84%);
|
||||
--ic-surface-low: rgba(255, 255, 255, 64%);
|
||||
--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 */
|
||||
--bg-overlay: rgba(0, 0, 0, 60%);
|
||||
|
@ -331,6 +331,10 @@
|
|||
--ic-surface-low: rgba(255, 251, 222, 64%);
|
||||
}
|
||||
|
||||
.font-monospace {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.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 parse from 'html-react-parser';
|
||||
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/';
|
||||
|
||||
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 = {
|
||||
replace: (node) => {
|
||||
const maths = node.attribs?.['data-mx-maths'];
|
||||
if (maths) {
|
||||
return (
|
||||
<Suspense fallback={<code>{maths}</code>}>
|
||||
<Math
|
||||
content={maths}
|
||||
throwOnError={false}
|
||||
errorColor="var(--tc-danger-normal)"
|
||||
displayMode={node.name === 'div'}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
function grabTheRightIcon(rawText) {
|
||||
// if variant is present as \uFE0F
|
||||
return twemoji.convert.toCodePoint(rawText.indexOf(U200D) < 0 ?
|
||||
rawText.replace(UFE0Fg, '') :
|
||||
rawText
|
||||
);
|
||||
}
|
||||
// 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 {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)
|
||||
* @param {boolean} [maths=false] - render maths (default: false)
|
||||
* @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;
|
||||
let content = text;
|
||||
const options = opts ?? { base: TWEMOJI_BASE_URL };
|
||||
|
@ -56,5 +61,5 @@ export function twemojify(text, opts, linkify = false, sanitize = true, maths =
|
|||
rel: 'noreferrer noopener',
|
||||
});
|
||||
}
|
||||
return parse(content, maths ? mathOptions : null);
|
||||
return parse(content, null);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { wasm } from '@rollup/plugin-wasm';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill';
|
||||
import inject from '@rollup/plugin-inject';
|
||||
import { svgLoader } from './viteSvgLoader';
|
||||
import { preact } from '@preact/preset-vite';
|
||||
|
||||
const copyFiles = {
|
||||
targets: [
|
||||
|
@ -39,20 +39,20 @@ export default defineConfig({
|
|||
viteStaticCopy(copyFiles),
|
||||
svgLoader(),
|
||||
wasm(),
|
||||
react(),
|
||||
preact(),
|
||||
],
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
},
|
||||
plugins: [
|
||||
// Enable esbuild polyfill plugins
|
||||
NodeGlobalsPolyfillPlugin({
|
||||
process: false,
|
||||
buffer: true,
|
||||
}),
|
||||
]
|
||||
define: {
|
||||
global: 'globalThis'
|
||||
},
|
||||
plugins: [
|
||||
// Enable esbuild polyfill plugins
|
||||
NodeGlobalsPolyfillPlugin({
|
||||
process: false,
|
||||
buffer: true,
|
||||
}),
|
||||
]
|
||||
}
|
||||
},
|
||||
build: {
|
||||
|
@ -62,7 +62,26 @@ export default defineConfig({
|
|||
rollupOptions: {
|
||||
plugins: [
|
||||
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