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

View file

@ -1,6 +1,7 @@
{
"defaultHomeserver": 3,
"defaultHomeserver": 0,
"homeserverList": [
"http://localhost:3000",
"converser.eu",
"envs.net",
"halogen.city",

View file

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
@ -7,22 +8,13 @@
<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 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 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 id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
@ -34,57 +26,18 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-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"
/>
<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;
@ -98,4 +51,5 @@
</audio>
<script type="module" src="./src/index.jsx"></script>
</body>
</html>

5331
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -4,7 +4,7 @@
display: inline-flex;
width: 42px;
height: 42px;
border-radius: var(--bo-radius);
border-radius: 50%;
position: relative;
&__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 './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>

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -85,12 +85,10 @@ const MessageHeader = React.memo(({
<span>{twemojify(username)}</span>
<span>{twemojify(userId)}</span>
</Text>
<div className="message__time">
<Text variant="b3">
<Text className="message__time" variant="b3">
<Time timestamp={timestamp} fullTime={fullTime} />
</Text>
</div>
</div>
));
MessageHeader.defaultProps = {
fullTime: false,
@ -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

View file

@ -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,15 +128,10 @@
}
& .message__time {
flex: 1;
display: flex;
justify-content: flex-end;
& > .text {
white-space: nowrap;
color: var(--tc-surface-low);
}
}
}
.message__reply {
&-wrapper {
min-height: 20px;

View file

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

View file

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

View file

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

View file

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

View file

@ -54,9 +54,6 @@
&:hover {
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 './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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,5 @@
min-height: 85px;
position: relative;
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 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}:`}

View file

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

View file

@ -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,7 +380,6 @@ function RoomViewInput({
/>
</Text>
</ScrollView>
</div>
<div ref={rightOptionsRef} className="room-input__option-container">
<IconButton
onClick={(e) => {
@ -404,6 +403,7 @@ function RoomViewInput({
}}
tooltip="Sticker"
src={StickerIC}
size="small"
/>
<IconButton
onClick={(e) => {
@ -414,8 +414,9 @@ function RoomViewInput({
}}
tooltip="Emoji"
src={EmojiIC}
size="small"
/>
<IconButton onClick={sendMessage} tooltip="Send" src={SendIC} />
</div>
</div>
</>
);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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%);
@ -251,18 +263,6 @@
--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%);
--bg-overlay-low: rgba(0, 0, 0, 80%);
@ -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);

View file

@ -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>
function grabTheRightIcon(rawText) {
// if variant is present as \uFE0F
return twemoji.convert.toCodePoint(rawText.indexOf(U200D) < 0 ?
rawText.replace(UFE0Fg, '') :
rawText
);
}
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 {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);
}

View file

@ -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,7 +39,7 @@ export default defineConfig({
viteStaticCopy(copyFiles),
svgLoader(),
wasm(),
react(),
preact(),
],
optimizeDeps: {
esbuildOptions: {
@ -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"
}
},
});

3219
yarn.lock Normal file

File diff suppressed because it is too large Load diff