Compare commits

...

14 commits

Author SHA1 Message Date
hippoz
66dbb1616d
quick fix 2023-03-26 01:55:46 +02:00
hippoz
212f18b69c
fix space manage 2023-03-26 01:06:03 +02:00
hippoz
197b15eb6d
improve bundle size and speed by using code splitting 2023-03-23 19:50:24 +02:00
hippoz
aec2c44c29
speed up application and reduce bundle size by switching to preact 2023-03-23 15:41:58 +02:00
hippoz
291dad7ef8
change user id colors to gruvbox material dark 2023-03-23 02:17:06 +02:00
hippoz
d193645b11
change scrollbar behavior to be more compatible 2023-03-23 02:00:09 +02:00
hippoz
a7a4b5b0f1
fix crash on permissions tab on non-standard matrix servers 2023-03-23 01:56:14 +02:00
hippoz
745cd87b7f
fix crashes and re-add twemoji 2023-03-23 01:48:08 +02:00
hippoz
b86967fc3c
increase room selector padding 2023-03-20 02:20:03 +02:00
hippoz
870de09b0e
improve profile viewer 2023-03-20 02:16:55 +02:00
hippoz
7dcdf98b4e
fix segmented controls 2023-03-20 02:12:18 +02:00
hippoz
34f4afab67
improve heading font sizes 2023-03-20 02:10:43 +02:00
hippoz
0d0ea8f09f
add readme clarification 2023-03-20 00:17:27 +02:00
hippoz
5bc5e3cbd2
visual improvements, remove various dependencies 2023-03-20 00:06:42 +02:00
49 changed files with 3546 additions and 5772 deletions

View file

@ -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)

View file

@ -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",

View file

@ -1,28 +1,20 @@
<!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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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,58 +26,19 @@
<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 </head>
rel="apple-touch-icon"
sizes="72x72" <body id="appBody">
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> <script>
window.global ||= window; window.global ||= window;
</script> </script>
@ -97,5 +50,6 @@
<source src="./public/sound/invite.ogg" type="audio/ogg" /> <source src="./public/sound/invite.ogg" type="audio/ogg" />
</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

File diff suppressed because it is too large Load diff

View file

@ -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"
} }
} }

View file

@ -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>
) )

View file

@ -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 {

View file

@ -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>

View file

@ -0,0 +1,6 @@
import Text from '../text/Text';
import './LoadingText.scss';
export function LoadingText() {
return <Text className='loading-text'>Loading...</Text>
}

View file

@ -0,0 +1,5 @@
.loading-text {
display: block;
padding: var(--sp-tight);
color: var(--tc-surface-low);
}

View file

@ -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;

View file

@ -1,3 +0,0 @@
.katex-display {
margin: 0 !important;
}

View file

@ -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;
} }
} }

View file

@ -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;

View file

@ -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,
}; };

View file

@ -9,6 +9,10 @@
} }
} }
.emoji {
content-visibility: auto;
}
.text { .text {
margin: 0; margin: 0;
padding: 0; padding: 0;

View file

@ -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>
); );
} }

View file

@ -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,
}; };

View file

@ -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

View file

@ -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,14 +128,9 @@
} }
& .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 {

View file

@ -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) {

View file

@ -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;
} }

View file

@ -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;

View file

@ -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 {

View file

@ -54,9 +54,6 @@
&:hover { &:hover {
background-color: transparent; background-color: transparent;
} }
& .message__time {
flex: 0;
}
} }
} }
} }

View file

@ -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>

View file

@ -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%;

View file

@ -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>
)} )}

View file

@ -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);
} }
} }

View file

@ -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;
} }

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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);
} }
} }

View file

@ -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}:`}

View file

@ -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} />

View file

@ -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>
</> </>
); );

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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>

View file

@ -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) {

View file

@ -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,

View file

@ -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;
} }
} }

View file

@ -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')
);

View file

@ -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);

View file

@ -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);
} }

View file

@ -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"
} }
}, },
}); });

3219
yarn.lock Normal file

File diff suppressed because it is too large Load diff