Compare commits

..
Author SHA1 Message Date
Ajay Bura d2a9e179f0 Fix bug
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-21 08:48:36 +05:30
Ajay Bura 321ee96620 Update string
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 22:03:55 +05:30
Ajay Bura ea98418e50 Redesign user profile in settings
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 21:59:54 +05:30
Ajay Bura 4265b97419 Redesign app settings
Signed-off-by: Ajay Bura <ajbura@gmail.com>
2022-03-20 19:10:06 +05:30
88 changed files with 1113 additions and 2677 deletions

No files matched your search

+3 -1
View File
@@ -1,5 +1,7 @@
name: 🐞 Bug Report
title: '[bug] '
description: Report a bug
labels: 'type: bug'
body:
- type: markdown
@@ -49,7 +51,7 @@ body:
render: shell
validations:
required: true
- type: textarea
id: context
attributes:
+3 -1
View File
@@ -1,5 +1,7 @@
name: 💡 Feature Request
title: '[feat] '
description: Suggest an idea
labels: 'type: feature'
body:
- type: textarea
@@ -25,7 +27,7 @@ body:
attributes:
label: Alternatives considered
description: "Any alternative solutions you've considered"
- type: textarea
id: context
attributes:
+11 -13
View File
@@ -1,30 +1,28 @@
# Docs: <https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 15
updates:
- package-ecosystem: github-actions
directory: /
schedule:
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
- package-ecosystem: docker
directory: /
schedule:
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: "tuesday"
time: "01:00"
timezone: "Asia/Kolkata"
open-pull-requests-limit: 5
+7 -2
View File
@@ -10,8 +10,8 @@ jobs:
env:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Check out the repo
uses: actions/checkout@v3.0.0
- name: Build app
run: npm ci && npm run build
- name: Upload artifact
@@ -32,3 +32,8 @@ jobs:
name: pr.json
path: pr.json
retention-days: 1
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false
+3 -3
View File
@@ -13,7 +13,7 @@ jobs:
# There's a 'download artifact' action but it hasn't been updated for the
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess:
- name: Download artifact
- name: 'Download artifact'
uses: actions/github-script@v6.0.0
with:
script: |
@@ -46,7 +46,7 @@ jobs:
fs.writeFileSync('${{github.workspace}}/pr.json.zip', Buffer.from(download.data));
- name: Extract Artifacts
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
- name: Read PR Info
- name: 'Read PR Info'
id: readctx
uses: actions/github-script@v6.0.0
with:
@@ -68,7 +68,7 @@ jobs:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
timeout-minutes: 1
- name: Edit PR Description
uses: Beakyn/gha-comment-pull-request@v1.0.2
uses: velas/pr-description@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
-21
View File
@@ -1,21 +0,0 @@
name: 'Docker check'
on:
pull_request:
paths:
- 'Dockerfile'
- '.github/workflows/docker-pr.yml'
jobs:
docker-build:
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{github.event.number}}
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Build Docker image
uses: docker/build-push-action@v2.10.0
with:
context: .
push: false
@@ -6,15 +6,13 @@ on:
- dev
jobs:
deploy-to-netlify:
deploy:
name: 'Deploy'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2
- uses: actions/checkout@v3.0.0
- uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
install_command: "npm ci"
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
@@ -9,8 +9,8 @@ jobs:
name: 'Deploy to Netlify'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Check out the repo
uses: actions/checkout@v3.0.0
- name: Build and deploy to Netlify
uses: jsmrcaga/action-netlify-deploy@v1.7.2
with:
@@ -26,7 +26,7 @@ jobs:
- name: Create tar.gz
run: tar -czvf cinny-${{ steps.vars.outputs.tag }}.tar.gz dist
- name: Upload tagged release
uses: softprops/action-gh-release@v0.1.14
uses: softprops/action-gh-release@v1
with:
files: |
cinny-${{ steps.vars.outputs.tag }}.tar.gz
@@ -35,8 +35,8 @@ jobs:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.1
- name: Check out the repo
uses: actions/checkout@v3.0.0
- name: Login to Docker Hub
uses: docker/login-action@v1.14.1
with:
@@ -44,7 +44,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v3.7.0
uses: docker/metadata-action@v3.6.2
with:
images: ajbura/cinny
- name: Build and push Docker image
+1 -1
View File
@@ -1,5 +1,5 @@
## Builder
FROM node:17.9.0-alpine3.15 as builder
FROM node:17.7.1-alpine3.15 as builder
WORKDIR /src
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Ajay Bura (ajbura) and contributors
Copyright (c) 2021 Ajay Bura (ajbura) and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+945 -654
View File
File diff suppressed because it is too large. Load diff
+15 -15
View File
@@ -15,8 +15,8 @@
"author": "Ajay Bura",
"license": "MIT",
"dependencies": {
"@fontsource/inter": "^4.5.7",
"@fontsource/roboto": "^4.5.5",
"@fontsource/inter": "^4.5.5",
"@fontsource/roboto": "^4.5.3",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@tippyjs/react": "^4.2.6",
"babel-polyfill": "^6.26.0",
@@ -26,9 +26,9 @@
"file-saver": "^2.0.5",
"flux": "^4.0.3",
"formik": "^2.2.9",
"html-react-parser": "^1.4.11",
"html-react-parser": "^1.4.8",
"linkifyjs": "^2.1.9",
"matrix-js-sdk": "^17.0.0",
"matrix-js-sdk": "^15.6.0",
"micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1",
"micromark-util-chunked": "^1.0.0",
@@ -37,21 +37,21 @@
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-autosize-textarea": "^7.1.0",
"react-dnd": "^15.1.2",
"react-dnd-html5-backend": "^15.1.3",
"react-dnd": "^15.1.1",
"react-dnd-html5-backend": "^15.1.2",
"react-dom": "^17.0.2",
"react-google-recaptcha": "^2.1.0",
"react-modal": "^3.14.4",
"sanitize-html": "^2.7.0",
"tippy.js": "^6.3.7",
"twemoji": "^14.0.2"
"twemoji": "^14.0.1"
},
"devDependencies": {
"@babel/core": "^7.17.9",
"@babel/core": "^7.17.7",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"assert": "^2.0.0",
"babel-loader": "^8.2.4",
"babel-loader": "^8.2.2",
"browserify-fs": "^1.0.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
@@ -59,27 +59,27 @@
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"eslint": "^8.13.0",
"eslint": "^8.11.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-react-hooks": "^4.2.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"html-loader": "^3.1.0",
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^2.6.0",
"path-browserify": "^1.0.1",
"sass": "^1.50.0",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.1",
"url": "^0.11.0",
"util": "^0.12.4",
"webpack": "^5.72.0",
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1",
"webpack-dev-server": "^4.7.4",
"webpack-merge": "^5.7.3"
}
}
+1 -4
View File
@@ -11,12 +11,11 @@ const IconButton = React.forwardRef(({
variant, size, type,
tooltip, tooltipPlacement, src,
onClick, tabIndex, disabled, isImage,
className,
}, ref) => {
const btn = (
<button
ref={ref}
className={`ic-btn ic-btn-${variant} ${className}`}
className={`ic-btn ic-btn-${variant}`}
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
onClick={onClick}
// eslint-disable-next-line react/button-has-type
@@ -48,7 +47,6 @@ IconButton.defaultProps = {
tabIndex: 0,
disabled: false,
isImage: false,
className: '',
};
IconButton.propTypes = {
@@ -62,7 +60,6 @@ IconButton.propTypes = {
tabIndex: PropTypes.number,
disabled: PropTypes.bool,
isImage: PropTypes.bool,
className: PropTypes.string,
};
export default IconButton;
-59
View File
@@ -1,59 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './InfoCard.scss';
import Text from '../text/Text';
import RawIcon from '../system-icons/RawIcon';
import IconButton from '../button/IconButton';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function InfoCard({
className, style,
variant, iconSrc,
title, content,
rounded, requestClose,
}) {
const classes = [`info-card info-card--${variant}`];
if (rounded) classes.push('info-card--rounded');
if (className) classes.push(className);
return (
<div className={classes.join(' ')} style={style}>
{iconSrc && (
<div className="info-card__icon">
<RawIcon color={`var(--ic-${variant}-high)`} src={iconSrc} />
</div>
)}
<div className="info-card__content">
<Text>{title}</Text>
{content}
</div>
{requestClose && (
<IconButton src={CrossIC} variant={variant} onClick={requestClose} />
)}
</div>
);
}
InfoCard.defaultProps = {
className: null,
style: null,
variant: 'surface',
iconSrc: null,
content: null,
rounded: false,
requestClose: null,
};
InfoCard.propTypes = {
className: PropTypes.string,
style: PropTypes.shape({}),
variant: PropTypes.oneOf(['surface', 'primary', 'positive', 'caution', 'danger']),
iconSrc: PropTypes.string,
title: PropTypes.string.isRequired,
content: PropTypes.node,
rounded: PropTypes.bool,
requestClose: PropTypes.func,
};
export default InfoCard;
-79
View File
@@ -1,79 +0,0 @@
@use '.././../partials/flex';
@use '.././../partials/dir';
.info-card {
display: flex;
align-items: flex-start;
line-height: 0;
padding: var(--sp-tight);
@include dir.prop(border-left, 4px solid transparent, none);
@include dir.prop(border-right, none, 4px solid transparent);
& > .ic-btn {
padding: 0;
border-radius: 4;
}
&__content {
margin: 0 var(--sp-tight);
@extend .cp-fx__item-one;
& > *:nth-child(2) {
margin-top: var(--sp-ultra-tight);
}
}
&--rounded {
@include dir.prop(
border-radius,
0 var(--bo-radius) var(--bo-radius) 0,
var(--bo-radius) 0 0 var(--bo-radius)
);
}
&--surface {
border-color: var(--bg-surface-border);
background-color: var(--bg-surface-hover);
}
&--primary {
border-color: var(--bg-primary);
background-color: var(--bg-primary-hover);
& .text {
color: var(--tc-primary-high);
&-b3 {
color: var(--tc-primary-normal);
}
}
}
&--positive {
border-color: var(--bg-positive-border);
background-color: var(--bg-positive-hover);
& .text {
color: var(--tc-positive-high);
&-b3 {
color: var(--tc-positive-normal);
}
}
}
&--caution {
border-color: var(--bg-caution-border);
background-color: var(--bg-caution-hover);
& .text {
color: var(--tc-caution-high);
&-b3 {
color: var(--tc-caution-normal);
}
}
}
&--danger {
border-color: var(--bg-danger-border);
background-color: var(--bg-danger-hover);
& .text {
color: var(--tc-danger-high);
&-b3 {
color: var(--tc-danger-normal);
}
}
}
}
-25
View File
@@ -1,25 +0,0 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from 'react';
import initMatrix from '../../client/initMatrix';
import { hasCrossSigningAccountData } from '../../util/matrixUtil';
export function useCrossSigningStatus() {
const mx = initMatrix.matrixClient;
const [isCSEnabled, setIsCSEnabled] = useState(hasCrossSigningAccountData());
useEffect(() => {
if (isCSEnabled) return null;
const handleAccountData = (event) => {
if (event.getType() === 'm.cross_signing.master') {
setIsCSEnabled(true);
}
};
mx.on('accountData', handleAccountData);
return () => {
mx.removeListener('accountData', handleAccountData);
};
}, [isCSEnabled === false]);
return isCSEnabled;
}
-32
View File
@@ -1,32 +0,0 @@
/* eslint-disable import/prefer-default-export */
import { useState, useEffect } from 'react';
import initMatrix from '../../client/initMatrix';
export function useDeviceList() {
const mx = initMatrix.matrixClient;
const [deviceList, setDeviceList] = useState(null);
useEffect(() => {
let isMounted = true;
const updateDevices = () => mx.getDevices().then((data) => {
if (!isMounted) return;
setDeviceList(data.devices || []);
});
updateDevices();
const handleDevicesUpdate = (users) => {
if (users.includes(mx.getUserId())) {
updateDevices();
}
};
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
return () => {
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
isMounted = false;
};
}, []);
return deviceList;
}
@@ -1,58 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import './ConfirmDialog.scss';
import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
function ConfirmDialog({
desc, actionTitle, actionType, onComplete,
}) {
return (
<div className="confirm-dialog">
<Text>{desc}</Text>
<div className="confirm-dialog__btn">
<Button variant={actionType} onClick={() => onComplete(true)}>{actionTitle}</Button>
<Button onClick={() => onComplete(false)}>Cancel</Button>
</div>
</div>
);
}
ConfirmDialog.propTypes = {
desc: PropTypes.string.isRequired,
actionTitle: PropTypes.string.isRequired,
actionType: PropTypes.oneOf(['primary', 'positive', 'danger', 'caution']).isRequired,
onComplete: PropTypes.func.isRequired,
};
/**
* @param {string} title title of confirm dialog
* @param {string} desc description of confirm dialog
* @param {string} actionTitle title of main action to take
* @param {'primary' | 'positive' | 'danger' | 'caution'} actionType type of action. default=primary
* @return {Promise<boolean>} does it get's confirmed or not
*/
// eslint-disable-next-line import/prefer-default-export
export const confirmDialog = (title, desc, actionTitle, actionType = 'primary') => new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<ConfirmDialog
desc={desc}
actionTitle={actionTitle}
actionType={actionType}
onComplete={(isConfirmed) => {
isCompleted = true;
resolve(isConfirmed);
requestClose();
}}
/>
),
() => {
if (!isCompleted) resolve(false);
},
);
});
@@ -1,11 +0,0 @@
.confirm-dialog {
padding: var(--sp-normal);
& > .text {
padding-bottom: var(--sp-normal);
}
&__btn {
display: flex;
gap: var(--sp-normal);
}
}
+2 -5
View File
@@ -12,11 +12,10 @@ import RawModal from '../../atoms/modal/RawModal';
function Dialog({
className, isOpen, title, onAfterOpen, onAfterClose,
contentOptions, onRequestClose, closeFromOutside, children,
invisibleScroll,
}) {
return (
<RawModal
className={`${className === null ? '' : `${className} `}dialog-modal`}
className={`${className === null ? '' : `${className} `}dialog-model`}
isOpen={isOpen}
onAfterOpen={onAfterOpen}
onAfterClose={onAfterClose}
@@ -37,7 +36,7 @@ function Dialog({
{contentOptions}
</Header>
<div className="dialog__content__wrapper">
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
<ScrollView autoHide>
<div className="dialog__content-container">
{children}
</div>
@@ -56,7 +55,6 @@ Dialog.defaultProps = {
onAfterClose: null,
onRequestClose: null,
closeFromOutside: true,
invisibleScroll: false,
};
Dialog.propTypes = {
@@ -69,7 +67,6 @@ Dialog.propTypes = {
onRequestClose: PropTypes.func,
closeFromOutside: PropTypes.bool,
children: PropTypes.node.isRequired,
invisibleScroll: PropTypes.bool,
};
export default Dialog;
+6 -1
View File
@@ -1,4 +1,4 @@
.dialog-modal {
.dialog-model {
--modal-height: 656px;
max-height: min(100%, var(--modal-height));
display: flex;
@@ -21,3 +21,8 @@
flex-direction: column;
}
}
.dialog__content-container {
padding-top: var(--sp-extra-tight);
padding-bottom: var(--sp-extra-loose);
}
@@ -1,49 +0,0 @@
import React, { useState, useEffect } from 'react';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import IconButton from '../../atoms/button/IconButton';
import Dialog from './Dialog';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function ReusableDialog() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
const handleOpen = (title, render, afterClose) => {
setIsOpen(true);
setData({ title, render, afterClose });
};
navigation.on(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
return () => {
navigation.removeListener(cons.events.navigation.REUSABLE_DIALOG_OPENED, handleOpen);
};
}, []);
const handleAfterClose = () => {
data.afterClose?.();
setData(null);
};
const handleRequestClose = () => {
setIsOpen(false);
};
return (
<Dialog
isOpen={isOpen}
title={data?.title || ''}
onAfterClose={handleAfterClose}
onRequestClose={handleRequestClose}
contentOptions={<IconButton src={CrossIC} onClick={handleRequestClose} tooltip="Close" />}
invisibleScroll
>
{data?.render(handleRequestClose) || <div />}
</Dialog>
);
}
export default ReusableDialog;
+7 -22
View File
@@ -36,8 +36,6 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import CmdIC from '../../../../public/res/ic/outlined/cmd.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function PlaceholderMessage() {
return (
<div className="ph-msg">
@@ -188,17 +186,9 @@ const MessageBody = React.memo(({
// if body is not string it is a React element.
if (typeof body !== 'string') return <div className="message__body">{body}</div>;
let content = null;
if (isCustomHTML) {
try {
content = twemojify(sanitizeCustomHtml(body), undefined, true, false);
} catch {
console.error('Malformed custom html: ', body);
content = twemojify(body, undefined);
}
} else {
content = twemojify(body, undefined, true);
}
let content = isCustomHTML
? twemojify(sanitizeCustomHtml(body), undefined, true, false)
: twemojify(body, undefined, true);
// Determine if this message should render with large emojis
// Criteria:
@@ -548,15 +538,10 @@ const MessageOptions = React.memo(({
<MenuItem
variant="danger"
iconSrc={BinIC}
onClick={async () => {
const isConfirmed = await confirmDialog(
'Delete message',
'Are you sure that you want to delete this message?',
'Delete',
'danger',
);
if (!isConfirmed) return;
redactEvent(roomId, mEvent.getId());
onClick={() => {
if (window.confirm('Are you sure you want to delete this event')) {
redactEvent(roomId, mEvent.getId());
}
}}
>
Delete
+1 -2
View File
@@ -1,7 +1,6 @@
@use '../../atoms/scroll/scrollbar';
@use '../../partials/text';
@use '../../partials/dir';
@use '../../partials/screen';
.message,
.ph-msg {
@@ -96,7 +95,7 @@
.message__reactions {
max-width: calc(100% - 88px);
min-width: 0;
@include screen.smallerThan(tabletBreakpoint) {
@media (max-width: 1124px) {
max-width: 100%;
}
}
@@ -58,8 +58,7 @@ function PopupWindow({
return (
<RawModal
className={`${className === null ? '' : `${className} `}pw-modal`}
overlayClassName="pw-modal__overlay"
className={`${className === null ? '' : `${className} `}pw-model`}
isOpen={isOpen}
onAfterClose={onAfterClose}
onRequestClose={onRequestClose}
@@ -1,18 +1,9 @@
@use '../../partials/dir';
@use '../../partials/screen';
.pw-modal {
--modal-height: 774px;
.pw-model {
--modal-height: 656px;
max-height: var(--modal-height) !important;
height: 100%;
@include screen.smallerThan(mobileBreakpoint) {
--modal-height: 100%;
border-radius: 0 !important;
&__overlay {
padding: 0 !important;
}
}
}
.pw {
@@ -81,4 +72,4 @@
@include dir.side(margin, 0, var(--sp-tight));
}
}
}
}
@@ -8,8 +8,6 @@ import Text from '../../atoms/text/Text';
import Toggle from '../../atoms/button/Toggle';
import SettingTile from '../setting-tile/SettingTile';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function RoomEncryption({ roomId }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
@@ -17,20 +15,17 @@ function RoomEncryption({ roomId }) {
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
const handleEncryptionEnable = async () => {
const handleEncryptionEnable = () => {
const joinRule = room.getJoinRule();
const confirmMsg1 = 'It is not recommended to add encryption in public room. Anyone can find and join public rooms, so anyone can read messages in them.';
const confirmMsg2 = 'Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly';
const isConfirmed1 = (joinRule === 'public')
? await confirmDialog('Enable encryption', confirmMsg1, 'Continue', 'caution')
: true;
if (!isConfirmed1) return;
if (await confirmDialog('Enable encryption', confirmMsg2, 'Enable', 'caution')) {
setIsEncrypted(true);
mx.sendStateEvent(roomId, 'm.room.encryption', {
algorithm: 'm.megolm.v1.aes-sha2',
});
if (joinRule === 'public' ? confirm(confirmMsg1) : true) {
if (confirm(confirmMsg2)) {
setIsEncrypted(true);
mx.sendStateEvent(roomId, 'm.room.encryption', {
algorithm: 'm.megolm.v1.aes-sha2',
});
}
}
};
@@ -17,19 +17,19 @@ const visibility = {
const items = [{
iconSrc: null,
text: 'Anyone (including guests)',
text: 'World readable (anyone can read)',
type: visibility.WORLD_READABLE,
}, {
iconSrc: null,
text: 'Members (all messages)',
text: 'Member shared (since the point in time of selecting this option)',
type: visibility.SHARED,
}, {
iconSrc: null,
text: 'Members (messages after invite)',
text: 'Member invited (since they were invited)',
type: visibility.INVITED,
}, {
iconSrc: null,
text: 'Members (messages after join)',
text: 'Member joined (since they joined)',
type: visibility.JOINED,
}];
@@ -87,7 +87,7 @@ function RoomHistoryVisibility({ roomId }) {
</MenuItem>
))
}
<Text variant="b3">Changes to history visibility will only apply to future messages. The visibility of existing history will have no effect.</Text>
<Text variant="b3">Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.</Text>
</div>
);
}
@@ -35,12 +35,7 @@ const items = [{
function setRoomNotifType(roomId, newType) {
const mx = initMatrix.matrixClient;
const { notifications } = initMatrix;
let roomPushRule;
try {
roomPushRule = mx.getRoomPushRule('global', roomId);
} catch {
roomPushRule = undefined;
}
const roomPushRule = mx.getRoomPushRule('global', roomId);
const promises = [];
if (newType === cons.notifs.MUTE) {
+5 -12
View File
@@ -15,8 +15,6 @@ import TickMarkIC from '../../../../public/res/ic/outlined/tick-mark.svg';
import AddUserIC from '../../../../public/res/ic/outlined/add-user.svg';
import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function RoomOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
@@ -31,16 +29,11 @@ function RoomOptions({ roomId, afterOptionSelect }) {
openInviteUser(roomId);
afterOptionSelect();
};
const handleLeaveClick = async () => {
afterOptionSelect();
const isConfirmed = await confirmDialog(
'Leave room',
`Are you sure that you want to leave "${room.name}" room?`,
'Leave',
'danger',
);
if (!isConfirmed) return;
roomActions.leave(roomId);
const handleLeaveClick = () => {
if (confirm('Are you really want to leave this room?')) {
roomActions.leave(roomId);
afterOptionSelect();
}
};
return (
@@ -179,7 +179,6 @@ function RoomPermissions({ roomId }) {
const pLEvent = room.currentState.getStateEvents('m.room.power_levels')[0];
const permissions = pLEvent.getContent();
const canChangePermission = room.currentState.maySendStateEvent('m.room.power_levels', mx.getUserId());
const myPowerLevel = room.getMember(mx.getUserId())?.powerLevel ?? 100;
const handlePowerSelector = (e, permKey, parentKey, powerLevel) => {
const handlePowerLevelChange = (newPowerLevel) => {
@@ -209,7 +208,7 @@ function RoomPermissions({ roomId }) {
(closeMenu) => (
<PowerLevelSelector
value={powerLevel}
max={myPowerLevel}
max={100}
onSelect={(pl) => {
closeMenu();
handlePowerLevelChange(pl);
@@ -19,7 +19,6 @@ import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import { useStore } from '../../hooks/useStore';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function RoomProfile({ roomId }) {
const isMountStore = useStore();
@@ -118,13 +117,7 @@ function RoomProfile({ roomId }) {
const handleAvatarUpload = async (url) => {
if (url === null) {
const isConfirmed = await confirmDialog(
'Remove avatar',
'Are you sure that you want to remove room avatar?',
'Remove',
'caution',
);
if (isConfirmed) {
if (confirm('Are you sure you want to remove avatar?')) {
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
}
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
+1
View File
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import './RoomTile.scss';
import { twemojify } from '../../../util/twemojify';
import { sanitizeText } from '../../../util/sanitize';
import colorMXID from '../../../util/colorMXID';
@@ -9,11 +9,7 @@ function SettingTile({ title, options, content }) {
<div className="setting-tile">
<div className="setting-tile__content">
<div className="setting-tile__title">
{
typeof title === 'string'
? <Text variant="b1">{title}</Text>
: title
}
<Text variant="b1">{title}</Text>
</div>
{content}
</div>
@@ -28,7 +24,7 @@ SettingTile.defaultProps = {
};
SettingTile.propTypes = {
title: PropTypes.node.isRequired,
title: PropTypes.string.isRequired,
options: PropTypes.node,
content: PropTypes.node,
};
@@ -9,12 +9,11 @@ import Tooltip from '../../atoms/tooltip/Tooltip';
import { blurOnBubbling } from '../../atoms/button/script';
const SidebarAvatar = React.forwardRef(({
className, tooltip, active, onClick,
onContextMenu, avatar, notificationBadge,
tooltip, active, onClick, onContextMenu,
avatar, notificationBadge,
}, ref) => {
const classes = ['sidebar-avatar'];
if (active) classes.push('sidebar-avatar--active');
if (className) classes.push(className);
let activeClass = '';
if (active) activeClass = ' sidebar-avatar--active';
return (
<Tooltip
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
@@ -22,7 +21,7 @@ const SidebarAvatar = React.forwardRef(({
>
<button
ref={ref}
className={classes.join(' ')}
className={`sidebar-avatar${activeClass}`}
type="button"
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
onClick={onClick}
@@ -35,7 +34,6 @@ const SidebarAvatar = React.forwardRef(({
);
});
SidebarAvatar.defaultProps = {
className: null,
active: false,
onClick: null,
onContextMenu: null,
@@ -43,7 +41,6 @@ SidebarAvatar.defaultProps = {
};
SidebarAvatar.propTypes = {
className: PropTypes.string,
tooltip: PropTypes.string.isRequired,
active: PropTypes.bool,
onClick: PropTypes.func,
@@ -24,8 +24,6 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import PinIC from '../../../../public/res/ic/outlined/pin.svg';
import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import { confirmDialog } from '../confirm-dialog/ConfirmDialog';
function SpaceOptions({ roomId, afterOptionSelect }) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
@@ -56,16 +54,11 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
afterOptionSelect();
};
const handleLeaveClick = async () => {
afterOptionSelect();
const isConfirmed = await confirmDialog(
'Leave space',
`Are you sure that you want to leave "${room.name}" space?`,
'Leave',
'danger',
);
if (!isConfirmed) return;
leave(roomId);
const handleLeaveClick = () => {
if (confirm('Are you really want to leave this space?')) {
leave(roomId);
afterOptionSelect();
}
};
return (
+1 -1
View File
@@ -210,7 +210,7 @@ function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
/>
)}
content={(
<Text variant="b3">Selecting Admin sets 100 power level whereas Founder sets 101.</Text>
<Text variant="b3">Override the default (100) power level.</Text>
)}
/>
<Input name="topic" minHeight={174} resizable label="Topic (optional)" />
@@ -2,7 +2,6 @@
@use '../../partials/dir';
.create-room {
margin: var(--sp-normal);
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
&__form > * {
+1 -1
View File
@@ -8,7 +8,7 @@ import Text from '../../atoms/text/Text';
function DragDrop({ isOpen }) {
return (
<RawModal
className="drag-drop__modal"
className="drag-drop__model"
overlayClassName="drag-drop__overlay"
isOpen={isOpen}
>
+1 -1
View File
@@ -1,4 +1,4 @@
.drag-drop__modal {
.drag-drop__model {
box-shadow: none;
text-align: center;
@@ -6,8 +6,6 @@
--emoji-board-height: 390px;
--emoji-board-width: 286px;
display: flex;
max-width: 90vw;
max-height: 90vh;
&__content {
@extend .cp-fx__item-one;
@@ -93,10 +91,6 @@
}
}
.emoji-row {
display: flex;
}
.emoji-group {
--emoji-padding: 6px;
position: relative;
+2 -4
View File
@@ -54,19 +54,17 @@ function InviteList({ isOpen, onRequestClose }) {
}, [procInvite]);
function renderRoomTile(roomId) {
const mx = initMatrix.matrixClient;
const myRoom = mx.getRoom(roomId);
const myRoom = initMatrix.matrixClient.getRoom(roomId);
const roomName = myRoom.name;
let roomAlias = myRoom.getCanonicalAlias();
if (roomAlias === null) roomAlias = myRoom.roomId;
const inviterName = myRoom.getMember(mx.getUserId())?.events?.member?.getSender?.() ?? '';
return (
<RoomTile
key={myRoom.roomId}
name={roomName}
avatarSrc={initMatrix.matrixClient.getRoom(roomId).getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop')}
id={roomAlias}
inviterName={inviterName}
inviterName={myRoom.getJoinedMembers()[0].userId}
options={
procInvite.has(myRoom.roomId)
? (<Spinner size="small" />)
-22
View File
@@ -14,7 +14,6 @@ import {
} from '../../../client/action/navigation';
import { moveSpaceShortcut } from '../../../client/action/accountData';
import { abbreviateNumber, getEventCords } from '../../../util/common';
import { isCrossVerified } from '../../../util/matrixUtil';
import Avatar from '../../atoms/avatar/Avatar';
import NotificationBadge from '../../atoms/badge/NotificationBadge';
@@ -27,12 +26,8 @@ import UserIC from '../../../../public/res/ic/outlined/user.svg';
import AddPinIC from '../../../../public/res/ic/outlined/add-pin.svg';
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import InviteIC from '../../../../public/res/ic/outlined/invite.svg';
import ShieldUserIC from '../../../../public/res/ic/outlined/shield-user.svg';
import { useSelectedTab } from '../../hooks/useSelectedTab';
import { useDeviceList } from '../../hooks/useDeviceList';
import { tabText as settingTabText } from '../settings/Settings';
function useNotificationUpdate() {
const { notifications } = initMatrix;
@@ -90,22 +85,6 @@ function ProfileAvatarMenu() {
);
}
function CrossSigninAlert() {
const deviceList = useDeviceList();
const unverified = deviceList?.filter((device) => !isCrossVerified(device.device_id));
if (!unverified?.length) return null;
return (
<SidebarAvatar
className="sidebar__cross-signin-alert"
tooltip={`${unverified.length} unverified sessions`}
onClick={() => openSettings(settingTabText.SECURITY)}
avatar={<Avatar iconSrc={ShieldUserIC} iconColor="var(--ic-danger-normal)" size="normal" />}
/>
);
}
function FeaturedTab() {
const { roomList, accountData, notifications } = initMatrix;
const [selectedTab] = useSelectedTab();
@@ -379,7 +358,6 @@ function SideBar() {
notificationBadge={<NotificationBadge alert content={totalInvites} />}
/>
)}
<CrossSigninAlert />
<ProfileAvatarMenu />
</div>
</div>
-17
View File
@@ -57,21 +57,4 @@
width: 24px;
height: 1px;
background-color: var(--bg-surface-border);
}
.sidebar__cross-signin-alert .avatar-container {
box-shadow: var(--bs-danger-border);
animation-name: pushRight;
animation-duration: 400ms;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes pushRight {
from {
transform: translateX(4px) scale(1);
}
to {
transform: translateX(0) scale(1);
}
}
@@ -12,7 +12,6 @@ import ImageUpload from '../../molecules/image-upload/ImageUpload';
import Input from '../../atoms/input/Input';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import './ProfileEditor.scss';
@@ -39,15 +38,9 @@ function ProfileEditor({ userId }) {
};
}, [userId]);
const handleAvatarUpload = async (url) => {
const handleAvatarUpload = (url) => {
if (url === null) {
const isConfirmed = await confirmDialog(
'Remove avatar',
'Are you sure that you want to remove avatar?',
'Remove',
'caution',
);
if (isConfirmed) {
if (confirm('Are you sure you want to remove avatar?')) {
mx.setAvatarUrl('');
setAvatarSrc(null);
}
@@ -32,7 +32,6 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
function ModerationTools({
roomId, userId,
@@ -363,7 +362,7 @@ function ProfileViewer() {
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
);
const handleChangePowerLevel = async (newPowerLevel) => {
const handleChangePowerLevel = (newPowerLevel) => {
if (newPowerLevel === powerLevel) return;
const SHARED_POWER_MSG = 'You will not be able to undo this change as you are promoting the user to have the same power level as yourself. Are you sure?';
const DEMOTING_MYSELF_MSG = 'You will not be able to undo this change as you are demoting yourself. Are you sure?';
@@ -371,14 +370,9 @@ function ProfileViewer() {
const isSharedPower = newPowerLevel === myPowerLevel;
const isDemotingMyself = userId === mx.getUserId();
if (isSharedPower || isDemotingMyself) {
const isConfirmed = await confirmDialog(
'Change power level',
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
'Change',
'caution',
);
if (!isConfirmed) return;
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
if (confirm(isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG)) {
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
}
} else {
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
}
@@ -4,7 +4,7 @@ import './PublicRooms.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import { selectRoom, selectTab } from '../../../client/action/navigation';
import { selectRoom } from '../../../client/action/navigation';
import * as roomActions from '../../../client/action/room';
import Text from '../../atoms/text/Text';
@@ -179,9 +179,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
}, [joiningRooms]);
function handleViewRoom(roomId) {
const room = initMatrix.matrixClient.getRoom(roomId);
if (room.isSpaceRoom()) selectTab(roomId);
else selectRoom(roomId);
selectRoom(roomId);
onRequestClose();
}
@@ -195,7 +193,7 @@ function PublicRooms({ isOpen, searchTerm, onRequestClose }) {
return rooms.map((room) => {
const alias = typeof room.canonical_alias === 'string' ? room.canonical_alias : room.room_id;
const name = typeof room.name === 'string' ? room.name : alias;
const isJoined = initMatrix.matrixClient.getRoom(room.room_id) !== null;
const isJoined = initMatrix.roomList.rooms.has(room.room_id);
return (
<RoomTile
key={room.room_id}
-4
View File
@@ -8,8 +8,6 @@ import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
import ReusableDialog from '../../molecules/dialog/ReusableDialog';
function Dialogs() {
return (
<>
@@ -20,8 +18,6 @@ function Dialogs() {
<CreateRoom />
<SpaceAddExisting />
<Search />
<ReusableDialog />
</>
);
}
@@ -64,11 +64,9 @@ function ReadReceipts() {
onRequestClose={() => setIsOpen(false)}
contentOptions={<IconButton src={CrossIC} onClick={() => setIsOpen(false)} tooltip="Close" />}
>
<div style={{ marginTop: 'var(--sp-tight)', marginBottom: 'var(--sp-extra-loose)' }}>
{
readers.map(renderPeople)
}
</div>
{
readers.map(renderPeople)
}
</Dialog>
);
}
+3 -7
View File
@@ -3,10 +3,9 @@ import './Room.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import settings from '../../../client/state/settings';
import RoomTimeline from '../../../client/state/RoomTimeline';
import navigation from '../../../client/state/navigation';
import { openNavigation } from '../../../client/action/navigation';
import Welcome from '../welcome/Welcome';
import RoomView from './RoomView';
@@ -54,10 +53,7 @@ function Room() {
}, []);
const { roomTimeline, eventId } = roomInfo;
if (roomTimeline === null) {
setTimeout(() => openNavigation());
return <Welcome />;
}
if (roomTimeline === null) return <Welcome />;
return (
<div className="room">
@@ -65,7 +61,7 @@ function Room() {
<RoomSettings roomId={roomTimeline.roomId} />
<RoomView roomTimeline={roomTimeline} eventId={eventId} />
</div>
{isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
{ isDrawer && <PeopleDrawer roomId={roomTimeline.roomId} />}
</div>
);
}
+1 -8
View File
@@ -1,5 +1,4 @@
@use '../../partials/flex';
@use '../../partials/screen';
.room {
@extend .cp-fx__row;
@@ -10,10 +9,4 @@
position: relative;
overflow: hidden;
}
}
.room .people-drawer {
@include screen.smallerThan(tabletBreakpoint) {
display: none;
}
}
}
+5 -11
View File
@@ -36,7 +36,6 @@ import LeaveArrowIC from '../../../../public/res/ic/outlined/leave-arrow.svg';
import ChevronTopIC from '../../../../public/res/ic/outlined/chevron-top.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
const tabText = {
GENERAL: 'General',
@@ -86,15 +85,10 @@ function GeneralSettings({ roomId }) {
</MenuItem>
<MenuItem
variant="danger"
onClick={async () => {
const isConfirmed = await confirmDialog(
'Leave room',
`Are you sure that you want to leave "${room.name}" room?`,
'Leave',
'danger',
);
if (!isConfirmed) return;
roomActions.leave(roomId);
onClick={() => {
if (confirm('Are you really want to leave this room?')) {
roomActions.leave(roomId);
}
}}
iconSrc={LeaveArrowIC}
>
@@ -129,7 +123,7 @@ function SecuritySettings({ roomId }) {
<RoomEncryption roomId={roomId} />
</div>
<div className="room-settings__card">
<MenuHeader>Message history visibility</MenuHeader>
<MenuHeader>Message history visibility (Who can read history)</MenuHeader>
<RoomHistoryVisibility roomId={roomId} />
</div>
</>
-8
View File
@@ -1,6 +1,4 @@
@use '../../partials/flex';
@use '../../partials/screen';
@use '../../partials/dir';
.room-view {
@extend .cp-fx__column;
@@ -20,12 +18,6 @@
box-shadow: var(--bs-popup);
}
& .header {
@include screen.smallerThan(mobileBreakpoint) {
padding: 0 var(--sp-tight);
}
}
&__content-wrapper {
@extend .cp-fx__item-one;
@extend .cp-fx__column;
+1 -3
View File
@@ -44,9 +44,7 @@ const commands = [{
}, {
name: 'leave',
description: 'Leave current room',
exe: (roomId) => {
roomActions.leave(roomId);
},
exe: (roomId) => roomActions.leave(roomId),
}, {
name: 'invite',
isOptions: true,
+1 -1
View File
@@ -10,7 +10,7 @@
&__info {
display: flex;
width: 40px;
@include dir.side(margin, 14px, 10px);
@include dir.side(margin, 10px, 14px);
& > * {
margin: auto;
+1 -1
View File
@@ -63,7 +63,7 @@ function genRoomIntro(mEvent, roomTimeline) {
avatarSrc={avatarSrc}
name={roomTimeline.room.name}
heading={`Welcome to ${roomTimeline.room.name}`}
desc={`This is the beginning of the ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
desc={`This is the beginning of ${roomTimeline.room.name} room.${typeof roomTopic !== 'undefined' ? (` Topic: ${roomTopic}`) : ''}`}
time={mEvent ? `Created at ${dateFormat(mEvent.getDate(), 'dd mmmm yyyy, hh:MM TT')}` : null}
/>
);
+2 -10
View File
@@ -8,7 +8,7 @@ import { blurOnBubbling } from '../../atoms/button/script';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation';
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
import { toggleRoomSettings, openReusableContextMenu } from '../../../client/action/navigation';
import { togglePeopleDrawer } from '../../../client/action/settings';
import colorMXID from '../../../util/colorMXID';
import { getEventCords } from '../../../util/common';
@@ -25,7 +25,6 @@ import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.s
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
import UserIC from '../../../../public/res/ic/outlined/user.svg';
import VerticalMenuIC from '../../../../public/res/ic/outlined/vertical-menu.svg';
import BackArrowIC from '../../../../public/res/ic/outlined/chevron-left.svg';
import { useForceUpdate } from '../../hooks/useForceUpdate';
@@ -74,12 +73,6 @@ function RoomViewHeader({ roomId }) {
return (
<Header>
<IconButton
src={BackArrowIC}
className="room-header__back-btn"
tooltip="Return to navigation"
onClick={() => openNavigation()}
/>
<button
ref={roomHeaderBtnRef}
className="room-header__btn"
@@ -94,8 +87,7 @@ function RoomViewHeader({ roomId }) {
<RawIcon src={ChevronBottomIC} />
</button>
<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__members-btn" onClick={() => toggleRoomSettings(tabText.MEMBERS)} tooltip="Members" src={UserIC} />
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
<IconButton
onClick={openRoomOptions}
tooltip="Options"
+1 -21
View File
@@ -1,6 +1,5 @@
@use '../../partials/flex';
@use '../../partials/dir';
@use '../../partials/screen';
.room-header__btn {
min-width: 0;
@@ -25,23 +24,4 @@
box-shadow: var(--bs-surface-outline);
outline: none;
}
}
.room-header__drawer-btn {
@include screen.smallerThan(tabletBreakpoint) {
display: none;
}
}
.room-header__members-btn {
@include screen.biggerThan(tabletBreakpoint) {
display: none;
}
}
.room-header__back-btn {
@include dir.side(margin, 0, var(--sp-tight));
@include screen.biggerThan(mobileBreakpoint) {
display: none;
}
}
}
+1 -1
View File
@@ -199,7 +199,7 @@ function Search() {
return (
<RawModal
className="search-dialog__modal dialog-modal"
className="search-dialog__model dialog-model"
isOpen={isOpen}
onAfterOpen={handleAfterOpen}
onAfterClose={handleAfterClose}
+1 -1
View File
@@ -1,6 +1,6 @@
@use '../../partials/dir';
.search-dialog__modal {
.search-dialog__model {
--modal-height: 380px;
height: 100%;
background-color: var(--bg-surface);
-117
View File
@@ -1,117 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './AuthRequest.scss';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
let lastUsedPassword;
const getAuthId = (password) => ({
type: 'm.login.password',
password,
identifier: {
type: 'm.id.user',
user: initMatrix.matrixClient.getUserId(),
},
});
function AuthRequest({ onComplete, makeRequest }) {
const [status, setStatus] = useState(false);
const mountStore = useStore();
const handleForm = async (e) => {
mountStore.setItem(true);
e.preventDefault();
const password = e.target.password.value;
if (password.trim() === '') return;
try {
setStatus({ ongoing: true });
await makeRequest(getAuthId(password));
lastUsedPassword = password;
if (!mountStore.getItem()) return;
onComplete(true);
} catch (err) {
lastUsedPassword = undefined;
if (!mountStore.getItem()) return;
if (err.errcode === 'M_FORBIDDEN') {
setStatus({ error: 'Wrong password. Please enter correct password.' });
return;
}
setStatus({ error: 'Request failed!' });
}
};
const handleChange = () => {
setStatus(false);
};
return (
<div className="auth-request">
<form onSubmit={handleForm}>
<Input
name="password"
label="Account password"
type="password"
onChange={handleChange}
required
/>
{status.ongoing && <Spinner size="small" />}
{status.error && <Text variant="b3">{status.error}</Text>}
{(status === false || status.error) && <Button variant="primary" type="submit" disabled={!!status.error}>Continue</Button>}
</form>
</div>
);
}
AuthRequest.propTypes = {
onComplete: PropTypes.func.isRequired,
makeRequest: PropTypes.func.isRequired,
};
/**
* @param {string} title Title of dialog
* @param {(auth) => void} makeRequest request to make
* @returns {Promise<boolean>} whether the request succeed or not.
*/
export const authRequest = async (title, makeRequest) => {
try {
const auth = lastUsedPassword ? getAuthId(lastUsedPassword) : undefined;
await makeRequest(auth);
return true;
} catch (e) {
lastUsedPassword = undefined;
if (e.httpStatus !== 401 || e.data?.flows === undefined) return false;
const { flows } = e.data;
const canUsePassword = flows.find((f) => f.stages.includes('m.login.password'));
if (!canUsePassword) return false;
return new Promise((resolve) => {
let isCompleted = false;
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<AuthRequest
onComplete={(done) => {
isCompleted = true;
resolve(done);
requestClose();
}}
makeRequest={makeRequest}
/>
),
() => {
if (!isCompleted) resolve(false);
},
);
});
}
};
export default AuthRequest;
@@ -1,12 +0,0 @@
.auth-request {
padding: var(--sp-normal);
& form > *:not(:first-child) {
margin-top: var(--sp-normal);
}
& .text-b3 {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
}
-223
View File
@@ -1,223 +0,0 @@
/* eslint-disable react/jsx-one-expression-per-line */
import React, { useState } from 'react';
import './CrossSigning.scss';
import FileSaver from 'file-saver';
import { Formik } from 'formik';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { copyToClipboard } from '../../../util/common';
import { clearSecretStorageKeys } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import { authRequest } from './AuthRequest';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
const failedDialog = () => {
const renderFailure = (requestClose) => (
<div className="cross-signing__failure">
<Text variant="h1">{twemojify('❌')}</Text>
<Text weight="medium">Failed to setup cross signing. Please try again.</Text>
<Button onClick={requestClose}>Close</Button>
</div>
);
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
renderFailure,
);
};
const securityKeyDialog = (key) => {
const downloadKey = () => {
const blob = new Blob([key.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii',
});
FileSaver.saveAs(blob, 'security-key.txt');
};
const copyKey = () => {
copyToClipboard(key.encodedPrivateKey);
};
const renderSecurityKey = () => (
<div className="cross-signing__key">
<Text weight="medium">Please save this security key somewhere safe.</Text>
<Text className="cross-signing__key-text">
{key.encodedPrivateKey}
</Text>
<div className="cross-signing__key-btn">
<Button variant="primary" onClick={() => copyKey(key)}>Copy</Button>
<Button onClick={() => downloadKey(key)}>Download</Button>
</div>
</div>
);
// Download automatically.
downloadKey();
openReusableDialog(
<Text variant="s1" weight="medium">Security Key</Text>,
() => renderSecurityKey(),
);
};
function CrossSigningSetup() {
const initialValues = { phrase: '', confirmPhrase: '' };
const [genWithPhrase, setGenWithPhrase] = useState(undefined);
const setup = async (securityPhrase = undefined) => {
const mx = initMatrix.matrixClient;
setGenWithPhrase(typeof securityPhrase === 'string');
const recoveryKey = await mx.createRecoveryKeyFromPassphrase(securityPhrase);
clearSecretStorageKeys();
await mx.bootstrapSecretStorage({
createSecretStorageKey: async () => recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
const authUploadDeviceSigningKeys = async (makeRequest) => {
const isDone = await authRequest('Setup cross signing', async (auth) => {
await makeRequest(auth);
});
setTimeout(() => {
if (isDone) securityKeyDialog(recoveryKey);
else failedDialog();
});
};
await mx.bootstrapCrossSigning({
authUploadDeviceSigningKeys,
setupNewCrossSigning: true,
});
};
const validator = (values) => {
const errors = {};
if (values.phrase === '12345678') {
errors.phrase = 'How about 87654321 ?';
}
if (values.phrase === '87654321') {
errors.phrase = 'Your are playing with 🔥';
}
const PHRASE_REGEX = /^([^\s]){8,127}$/;
if (values.phrase.length > 0 && !PHRASE_REGEX.test(values.phrase)) {
errors.phrase = 'Phrase must contain 8-127 characters with no space.';
}
if (values.confirmPhrase.length > 0 && values.confirmPhrase !== values.phrase) {
errors.confirmPhrase = 'Phrase don\'t match.';
}
return errors;
};
return (
<div className="cross-signing__setup">
<div className="cross-signing__setup-entry">
<Text>
We will generate a <b>Security Key</b>,
which you can use to manage messages backup and session verification.
</Text>
{genWithPhrase !== false && <Button variant="primary" onClick={() => setup()} disabled={genWithPhrase !== undefined}>Generate Key</Button>}
{genWithPhrase === false && <Spinner size="small" />}
</div>
<Text className="cross-signing__setup-divider">OR</Text>
<Formik
initialValues={initialValues}
onSubmit={(values) => setup(values.phrase)}
validate={validator}
>
{({
values, errors, handleChange, handleSubmit,
}) => (
<form
className="cross-signing__setup-entry"
onSubmit={handleSubmit}
disabled={genWithPhrase !== undefined}
>
<Text>
Alternatively you can also set a <b>Security Phrase </b>
so you don't have to remember long Security Key,
and optionally save the Key as backup.
</Text>
<Input
name="phrase"
value={values.phrase}
onChange={handleChange}
label="Security Phrase"
type="password"
required
disabled={genWithPhrase !== undefined}
/>
{errors.phrase && <Text variant="b3" className="cross-signing__error">{errors.phrase}</Text>}
<Input
name="confirmPhrase"
value={values.confirmPhrase}
onChange={handleChange}
label="Confirm Security Phrase"
type="password"
required
disabled={genWithPhrase !== undefined}
/>
{errors.confirmPhrase && <Text variant="b3" className="cross-signing__error">{errors.confirmPhrase}</Text>}
{genWithPhrase !== true && <Button variant="primary" type="submit" disabled={genWithPhrase !== undefined}>Set Phrase & Generate Key</Button>}
{genWithPhrase === true && <Spinner size="small" />}
</form>
)}
</Formik>
</div>
);
}
const setupDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Setup cross signing</Text>,
() => <CrossSigningSetup />,
);
};
function CrossSigningReset() {
return (
<div className="cross-signing__reset">
<Text variant="h1">{twemojify('✋🧑‍🚒🤚')}</Text>
<Text weight="medium">Resetting cross-signing keys is permanent.</Text>
<Text>
Anyone you have verified with will see security alerts and your message backup will lost.
You almost certainly do not want to do this,
unless you have lost <b>Security Key</b> or <b>Phrase</b> and
every session you can cross-sign from.
</Text>
<Button variant="danger" onClick={setupDialog}>Reset</Button>
</div>
);
}
const resetDialog = () => {
openReusableDialog(
<Text variant="s1" weight="medium">Reset cross signing</Text>,
() => <CrossSigningReset />,
);
};
function CrossSignin() {
const isCSEnabled = useCrossSigningStatus();
return (
<SettingTile
title="Cross signing"
content={<Text variant="b3">Setup to verify and keep track of all your sessions. Also required to backup encrypted message.</Text>}
options={(
isCSEnabled
? <Button variant="danger" onClick={resetDialog}>Reset</Button>
: <Button variant="primary" onClick={setupDialog}>Setup</Button>
)}
/>
);
}
export default CrossSignin;
@@ -1,55 +0,0 @@
.cross-signing {
&__setup {
padding: var(--sp-normal);
}
&__setup-entry {
& > *:not(:first-child) {
margin-top: var(--sp-normal);
}
}
&__error {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
&__setup-divider {
margin: var(--sp-tight) 0;
display: flex;
align-items: center;
&::before,
&::after {
flex: 1;
content: '';
margin: var(--sp-tight) 0;
border-bottom: 1px solid var(--bg-surface-border);
}
}
}
.cross-signing__key {
padding: var(--sp-normal);
&-text {
margin: var(--sp-normal) 0;
padding: var(--sp-extra-tight);
background-color: var(--bg-surface-low);
border-radius: var(--bo-radius);
}
&-btn {
display: flex;
& > button:last-child {
margin: 0 var(--sp-normal);
}
}
}
.cross-signing__failure,
.cross-signing__reset {
padding: var(--sp-normal);
padding-top: var(--sp-extra-loose);
& > .text {
padding-bottom: var(--sp-normal);
}
}
-216
View File
@@ -1,216 +0,0 @@
import React, { useState, useEffect } from 'react';
import './DeviceManage.scss';
import dateFormat from 'dateformat';
import initMatrix from '../../../client/initMatrix';
import { isCrossVerified } from '../../../util/matrixUtil';
import { openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import Input from '../../atoms/input/Input';
import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
import InfoCard from '../../atoms/card/InfoCard';
import Spinner from '../../atoms/spinner/Spinner';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import { authRequest } from './AuthRequest';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useStore } from '../../hooks/useStore';
import { useDeviceList } from '../../hooks/useDeviceList';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
let isCompleted = false;
const renderContent = (onComplete) => {
const handleSubmit = (e) => {
e.preventDefault();
const name = e.target.session.value;
if (typeof name !== 'string') onComplete(null);
onComplete(name);
};
return (
<form className="device-manage__rename" onSubmit={handleSubmit}>
<Input value={deviceName} label="Session name" name="session" />
<div className="device-manage__rename-btn">
<Button variant="primary" type="submit">Save</Button>
<Button onClick={() => onComplete(null)}>Cancel</Button>
</div>
</form>
);
};
openReusableDialog(
<Text variant="s1" weight="medium">Edit session name</Text>,
(requestClose) => renderContent((name) => {
isCompleted = true;
resolve(name);
requestClose();
}),
() => {
if (!isCompleted) resolve(null);
},
);
});
function DeviceManage() {
const TRUNCATED_COUNT = 4;
const mx = initMatrix.matrixClient;
const isCSEnabled = useCrossSigningStatus();
const deviceList = useDeviceList();
const [processing, setProcessing] = useState([]);
const [truncated, setTruncated] = useState(true);
const mountStore = useStore();
mountStore.setItem(true);
useEffect(() => {
setProcessing([]);
}, [deviceList]);
const addToProcessing = (device) => {
const old = [...processing];
old.push(device.device_id);
setProcessing(old);
};
const removeFromProcessing = () => {
setProcessing([]);
};
if (deviceList === null) {
return (
<div className="device-manage">
<div className="device-manage__loading">
<Spinner size="small" />
<Text>Loading devices...</Text>
</div>
</div>
);
}
const handleRename = async (device) => {
const newName = await promptDeviceName(device.display_name);
if (newName === null || newName.trim() === '') return;
if (newName.trim() === device.display_name) return;
addToProcessing(device);
try {
await mx.setDeviceDetails(device.device_id, {
display_name: newName,
});
} catch {
if (!mountStore.getItem()) return;
removeFromProcessing(device);
}
};
const handleRemove = async (device) => {
const isConfirmed = await confirmDialog(
`Logout ${device.display_name}`,
`You are about to logout "${device.display_name}" session.`,
'Logout',
'danger',
);
if (!isConfirmed) return;
addToProcessing(device);
await authRequest(`Logout "${device.display_name}"`, async (auth) => {
await mx.deleteDevice(device.device_id, auth);
});
if (!mountStore.getItem()) return;
removeFromProcessing(device);
};
const renderDevice = (device, isVerified) => {
const deviceId = device.device_id;
const displayName = device.display_name;
const lastIP = device.last_seen_ip;
const lastTS = device.last_seen_ts;
return (
<SettingTile
key={deviceId}
title={(
<Text style={{ color: isVerified ? '' : 'var(--tc-danger-high)' }}>
{displayName}
<Text variant="b3" span>{` — ${deviceId}${mx.deviceId === deviceId ? ' (current)' : ''}`}</Text>
</Text>
)}
options={
processing.includes(deviceId)
? <Spinner size="small" />
: (
<>
<IconButton size="small" onClick={() => handleRename(device)} src={PencilIC} tooltip="Rename" />
<IconButton size="small" onClick={() => handleRemove(device)} src={BinIC} tooltip="Remove session" />
</>
)
}
content={(
<Text variant="b3">
Last activity
<span style={{ color: 'var(--tc-surface-normal)' }}>
{dateFormat(new Date(lastTS), ' hh:MM TT, dd/mm/yyyy')}
</span>
{lastIP ? ` at ${lastIP}` : ''}
</Text>
)}
/>
);
};
const unverified = [];
const verified = [];
deviceList.sort((a, b) => b.last_seen_ts - a.last_seen_ts).forEach((device) => {
if (isCrossVerified(device.device_id)) verified.push(device);
else unverified.push(device);
});
return (
<div className="device-manage">
<div>
<MenuHeader>Unverified sessions</MenuHeader>
{!isCSEnabled && (
<div style={{ padding: 'var(--sp-extra-tight) var(--sp-normal)' }}>
<InfoCard
rounded
variant="caution"
iconSrc={InfoIC}
title="Setup cross signing in case you lose all your sessions."
/>
</div>
)}
{
unverified.length > 0
? unverified.map((device) => renderDevice(device, false))
: <Text className="device-manage__info">No unverified session</Text>
}
</div>
<div>
<MenuHeader>Verified sessions</MenuHeader>
{
verified.length > 0
? verified.map((device, index) => {
if (truncated && index >= TRUNCATED_COUNT) return null;
return renderDevice(device, true);
})
: <Text className="device-manage__info">No verified session</Text>
}
{ verified.length > TRUNCATED_COUNT && (
<Button className="device-manage__info" onClick={() => setTruncated(!truncated)}>
{truncated ? `View ${verified.length - 4} more` : 'View less'}
</Button>
)}
{ deviceList.length > 0 && (
<Text className="device-manage__info" variant="b3">Session names are visible to everyone, so do not put any private info here.</Text>
)}
</div>
</div>
);
}
export default DeviceManage;
@@ -1,29 +0,0 @@
@use '../../partials/flex';
.device-manage {
&__loading {
@extend .cp-fx__row--c-c;
padding: var(--sp-extra-loose) var(--sp-normal);
.text {
margin: 0 var(--sp-normal);
}
}
&__info {
margin: var(--sp-normal);
}
& .setting-tile:last-of-type {
border-bottom: none;
}
&__rename {
padding: var(--sp-normal);
& > *:not(:last-child) {
margin-bottom: var(--sp-normal);
}
&-btn {
display: flex;
gap: var(--sp-normal);
}
}
}
-288
View File
@@ -1,288 +0,0 @@
/* eslint-disable react/prop-types */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './KeyBackup.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { deletePrivateKey } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import IconButton from '../../atoms/button/IconButton';
import Spinner from '../../atoms/spinner/Spinner';
import InfoCard from '../../atoms/card/InfoCard';
import SettingTile from '../../molecules/setting-tile/SettingTile';
import { accessSecretStorage } from './SecretStorageAccess';
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import BinIC from '../../../../public/res/ic/outlined/bin.svg';
import DownloadIC from '../../../../public/res/ic/outlined/download.svg';
import { useStore } from '../../hooks/useStore';
import { useCrossSigningStatus } from '../../hooks/useCrossSigningStatus';
function CreateKeyBackupDialog({ keyData }) {
const [done, setDone] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const doBackup = async () => {
setDone(false);
let info;
try {
info = await mx.prepareKeyBackupVersion(
null,
{ secureSecretStorage: true },
);
info = await mx.createKeyBackupVersion(info);
await mx.scheduleAllGroupSessionsForBackup();
if (!mountStore.getItem()) return;
setDone(true);
} catch (e) {
deletePrivateKey(keyData.keyId);
await mx.deleteKeyBackupVersion(info.version);
if (!mountStore.getItem()) return;
setDone(null);
}
};
useEffect(() => {
mountStore.setItem(true);
doBackup();
}, []);
return (
<div className="key-backup__create">
{done === false && (
<div>
<Spinner size="small" />
<Text>Creating backup...</Text>
</div>
)}
{done === true && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text>Successfully created backup</Text>
</>
)}
{done === null && (
<>
<Text>Failed to create backup</Text>
<Button onClick={doBackup}>Retry</Button>
</>
)}
</div>
);
}
CreateKeyBackupDialog.propTypes = {
keyData: PropTypes.shape({}).isRequired,
};
function RestoreKeyBackupDialog({ keyData }) {
const [status, setStatus] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
const restoreBackup = async () => {
setStatus(false);
let meBreath = true;
const progressCallback = (progress) => {
if (!progress.successes) return;
if (meBreath === false) return;
meBreath = false;
setTimeout(() => {
meBreath = true;
}, 200);
setStatus({ message: `Restoring backup keys... (${progress.successes}/${progress.total})` });
};
try {
const backupInfo = await mx.getKeyBackupVersion();
const info = await mx.restoreKeyBackupWithSecretStorage(
backupInfo,
undefined,
undefined,
{ progressCallback },
);
if (!mountStore.getItem()) return;
setStatus({ done: `Successfully restored backup keys (${info.imported}/${info.total}).` });
} catch (e) {
if (!mountStore.getItem()) return;
if (e.errcode === 'RESTORE_BACKUP_ERROR_BAD_KEY') {
deletePrivateKey(keyData.keyId);
setStatus({ error: 'Failed to restore backup. Key is invalid!', errorCode: 'BAD_KEY' });
} else {
setStatus({ error: 'Failed to restore backup.', errCode: 'UNKNOWN' });
}
}
};
useEffect(() => {
mountStore.setItem(true);
restoreBackup();
}, []);
return (
<div className="key-backup__restore">
{(status === false || status.message) && (
<div>
<Spinner size="small" />
<Text>{status.message ?? 'Restoring backup keys...'}</Text>
</div>
)}
{status.done && (
<>
<Text variant="h1">{twemojify('✅')}</Text>
<Text>{status.done}</Text>
</>
)}
{status.error && (
<>
<Text>{status.error}</Text>
<Button onClick={restoreBackup}>Retry</Button>
</>
)}
</div>
);
}
RestoreKeyBackupDialog.propTypes = {
keyData: PropTypes.shape({}).isRequired,
};
function DeleteKeyBackupDialog({ requestClose }) {
const [isDeleting, setIsDeleting] = useState(false);
const mx = initMatrix.matrixClient;
const mountStore = useStore();
mountStore.setItem(true);
const deleteBackup = async () => {
setIsDeleting(true);
try {
const backupInfo = await mx.getKeyBackupVersion();
if (backupInfo) await mx.deleteKeyBackupVersion(backupInfo.version);
if (!mountStore.getItem()) return;
requestClose(true);
} catch {
if (!mountStore.getItem()) return;
setIsDeleting(false);
}
};
return (
<div className="key-backup__delete">
<Text variant="h1">{twemojify('🗑')}</Text>
<Text weight="medium">Deleting key backup is permanent.</Text>
<Text>All encrypted messages keys stored on server will be deleted.</Text>
{
isDeleting
? <Spinner size="small" />
: <Button variant="danger" onClick={deleteBackup}>Delete</Button>
}
</div>
);
}
DeleteKeyBackupDialog.propTypes = {
requestClose: PropTypes.func.isRequired,
};
function KeyBackup() {
const mx = initMatrix.matrixClient;
const isCSEnabled = useCrossSigningStatus();
const [keyBackup, setKeyBackup] = useState(undefined);
const mountStore = useStore();
const fetchKeyBackupVersion = async () => {
const info = await mx.getKeyBackupVersion();
if (!mountStore.getItem()) return;
setKeyBackup(info);
};
useEffect(() => {
mountStore.setItem(true);
fetchKeyBackupVersion();
const handleAccountData = (event) => {
if (event.getType() === 'm.megolm_backup.v1') {
fetchKeyBackupVersion();
}
};
mx.on('accountData', handleAccountData);
return () => {
mx.removeListener('accountData', handleAccountData);
};
}, [isCSEnabled]);
const openCreateKeyBackup = async () => {
const keyData = await accessSecretStorage('Create Key Backup');
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Create Key Backup</Text>,
() => <CreateKeyBackupDialog keyData={keyData} />,
() => fetchKeyBackupVersion(),
);
};
const openRestoreKeyBackup = async () => {
const keyData = await accessSecretStorage('Restore Key Backup');
if (keyData === null) return;
openReusableDialog(
<Text variant="s1" weight="medium">Restore Key Backup</Text>,
() => <RestoreKeyBackupDialog keyData={keyData} />,
);
};
const openDeleteKeyBackup = () => openReusableDialog(
<Text variant="s1" weight="medium">Delete Key Backup</Text>,
(requestClose) => (
<DeleteKeyBackupDialog
requestClose={(isDone) => {
if (isDone) setKeyBackup(null);
requestClose();
}}
/>
),
);
const renderOptions = () => {
if (keyBackup === undefined) return <Spinner size="small" />;
if (keyBackup === null) return <Button variant="primary" onClick={openCreateKeyBackup}>Create Backup</Button>;
return (
<>
<IconButton src={DownloadIC} variant="positive" onClick={openRestoreKeyBackup} tooltip="Restore backup" />
<IconButton src={BinIC} onClick={openDeleteKeyBackup} tooltip="Delete backup" />
</>
);
};
return (
<SettingTile
title="Encrypted messages backup"
content={(
<>
<Text variant="b3">Online backup your encrypted messages keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.</Text>
{!isCSEnabled && (
<InfoCard
style={{ marginTop: 'var(--sp-ultra-tight)' }}
rounded
variant="caution"
iconSrc={InfoIC}
title="Setup cross signing to backup your encrypted messages."
/>
)}
</>
)}
options={isCSEnabled ? renderOptions() : null}
/>
);
}
export default KeyBackup;
-27
View File
@@ -1,27 +0,0 @@
.key-backup__create,
.key-backup__restore {
padding: var(--sp-normal);
& > div {
padding: var(--sp-normal) 0;
display: flex;
align-items: center;
& > .text {
margin: 0 var(--sp-normal);
}
}
& > .text {
margin-bottom: var(--sp-normal);
}
}
.key-backup__delete {
padding: var(--sp-normal);
padding-top: var(--sp-extra-loose);
& > .text {
padding-bottom: var(--sp-normal);
}
}
@@ -1,133 +0,0 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './SecretStorageAccess.scss';
import { deriveKey } from 'matrix-js-sdk/lib/crypto/key_passphrase';
import initMatrix from '../../../client/initMatrix';
import { openReusableDialog } from '../../../client/action/navigation';
import { getDefaultSSKey, getSSKeyInfo } from '../../../util/matrixUtil';
import { storePrivateKey, hasPrivateKey, getPrivateKey } from '../../../client/state/secretStorageKeys';
import Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button';
import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner';
import { useStore } from '../../hooks/useStore';
function SecretStorageAccess({ onComplete }) {
const mx = initMatrix.matrixClient;
const sSKeyId = getDefaultSSKey();
const sSKeyInfo = getSSKeyInfo(sSKeyId);
const isPassphrase = !!sSKeyInfo.passphrase;
const [withPhrase, setWithPhrase] = useState(isPassphrase);
const [process, setProcess] = useState(false);
const [error, setError] = useState(null);
const mountStore = useStore();
mountStore.setItem(true);
const toggleWithPhrase = () => setWithPhrase(!withPhrase);
const processInput = async ({ key, phrase }) => {
setProcess(true);
try {
const { salt, iterations } = sSKeyInfo.passphrase;
const privateKey = key
? mx.keyBackupKeyFromRecoveryKey(key)
: await deriveKey(phrase, salt, iterations);
const isCorrect = await mx.checkSecretStorageKey(privateKey, sSKeyInfo);
if (!mountStore.getItem()) return;
if (!isCorrect) {
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
setProcess(false);
return;
}
onComplete({
keyId: sSKeyId,
key,
phrase,
privateKey,
});
} catch (e) {
if (!mountStore.getItem()) return;
setError(`Incorrect Security ${key ? 'Key' : 'Phrase'}`);
setProcess(false);
}
};
const handleForm = async (e) => {
e.preventDefault();
const password = e.target.password.value;
if (password.trim() === '') return;
const data = {};
if (withPhrase) data.phrase = password;
else data.key = password;
processInput(data);
};
const handleChange = () => {
setError(null);
setProcess(false);
};
return (
<div className="secret-storage-access">
<form onSubmit={handleForm}>
<Input
name="password"
label={`Security ${withPhrase ? 'Phrase' : 'Key'}`}
type="password"
onChange={handleChange}
required
/>
{error && <Text variant="b3">{error}</Text>}
{!process && (
<div className="secret-storage-access__btn">
<Button variant="primary" type="submit">Continue</Button>
{isPassphrase && <Button onClick={toggleWithPhrase}>{`Use Security ${withPhrase ? 'Key' : 'Phrase'}`}</Button>}
</div>
)}
</form>
{process && <Spinner size="small" />}
</div>
);
}
SecretStorageAccess.propTypes = {
onComplete: PropTypes.func.isRequired,
};
/**
* @param {string} title Title of secret storage access dialog
* @returns {Promise<keyData | null>} resolve to keyData or null
*/
export const accessSecretStorage = (title) => new Promise((resolve) => {
let isCompleted = false;
const defaultSSKey = getDefaultSSKey();
if (hasPrivateKey(defaultSSKey)) {
resolve({ keyId: defaultSSKey, privateKey: getPrivateKey(defaultSSKey) });
return;
}
const handleComplete = (keyData) => {
isCompleted = true;
storePrivateKey(keyData.keyId, keyData.privateKey);
resolve(keyData);
};
openReusableDialog(
<Text variant="s1" weight="medium">{title}</Text>,
(requestClose) => (
<SecretStorageAccess
onComplete={(keyData) => {
handleComplete(keyData);
requestClose(requestClose);
}}
/>
),
() => {
if (!isCompleted) resolve(null);
},
);
});
export default SecretStorageAccess;
@@ -1,20 +0,0 @@
.secret-storage-access {
padding: var(--sp-normal);
& form > *:not(:first-child) {
margin-top: var(--sp-normal);
}
& .text-b3 {
color: var(--tc-danger-high);
margin-top: var(--sp-ultra-tight) !important;
}
&__btn {
display: flex;
justify-content: space-between;
}
& .donut-spinner {
margin-top: var(--sp-normal);
}
}
+12 -14
View File
@@ -26,9 +26,6 @@ import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/Impor
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import ProfileEditor from '../profile-editor/ProfileEditor';
import CrossSigning from './CrossSigning';
import KeyBackup from './KeyBackup';
import DeviceManage from './DeviceManage';
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
@@ -38,7 +35,6 @@ import PowerIC from '../../../../public/res/ic/outlined/power.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import CinnySVG from '../../../../public/res/svg/cinny.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
function AppearanceSection() {
const [, updateState] = useState({});
@@ -171,13 +167,17 @@ function SecuritySection() {
return (
<div className="settings-security">
<div className="settings-security__card">
<MenuHeader>Cross signing and backup</MenuHeader>
<CrossSigning />
<KeyBackup />
<MenuHeader>Device Info</MenuHeader>
<SettingTile
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
/>
<SettingTile
title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
/>
</div>
<DeviceManage />
<div className="settings-security__card">
<MenuHeader>Export/Import encryption keys</MenuHeader>
<MenuHeader>Encryption</MenuHeader>
<SettingTile
title="Export E2E room keys"
content={(
@@ -245,7 +245,7 @@ function AboutSection() {
);
}
export const tabText = {
const tabText = {
APPEARANCE: 'Appearance',
NOTIFICATIONS: 'Notifications',
SECURITY: 'Security',
@@ -298,10 +298,8 @@ function Settings() {
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
const handleLogout = async () => {
if (await confirmDialog('Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger')) {
logout();
}
const handleLogout = () => {
if (confirm('Confirm logout')) logout();
};
return (
+1 -3
View File
@@ -1,6 +1,5 @@
@use '../../partials/flex';
@use '../../partials/dir';
@use '../../partials/screen';
.settings-window {
& .pw {
@@ -39,7 +38,6 @@
.settings-appearance__card,
.settings-notifications,
.settings-security__card,
.settings-security .device-manage,
.settings-about__card {
@extend .settings-window__card;
}
@@ -87,4 +85,4 @@
margin: var(--sp-extra-tight) 0;
}
}
}
}
@@ -36,7 +36,6 @@ import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
import CategoryIC from '../../../../public/res/ic/outlined/category.svg';
import CategoryFilledIC from '../../../../public/res/ic/filled/category.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useForceUpdate } from '../../hooks/useForceUpdate';
const tabText = {
@@ -62,7 +61,6 @@ const tabItems = [{
function GeneralSettings({ roomId }) {
const isPinned = initMatrix.accountData.spaceShortcut.has(roomId);
const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId);
const roomName = initMatrix.matrixClient.getRoom(roomId)?.name;
const [, forceUpdate] = useForceUpdate();
return (
@@ -91,14 +89,10 @@ function GeneralSettings({ roomId }) {
</MenuItem>
<MenuItem
variant="danger"
onClick={async () => {
const isConfirmed = await confirmDialog(
'Leave space',
`Are you sure that you want to leave "${roomName}" space?`,
'Leave',
'danger',
);
if (isConfirmed) leave(roomId);
onClick={() => {
if (confirm('Are you really want to leave this space?')) {
leave(roomId);
}
}}
iconSrc={LeaveArrowIC}
>
-28
View File
@@ -1,28 +0,0 @@
$breakpoint-tablet: 1124px;
$breakpoint-mobile: 750px;
@mixin smallerThan($deviceBreakpoint) {
@if $deviceBreakpoint==mobileBreakpoint {
@media screen and (max-width: $breakpoint-mobile) {
@content;
}
}
@else if $deviceBreakpoint==tabletBreakpoint {
@media screen and (max-width: $breakpoint-tablet) {
@content;
}
}
}
@mixin biggerThan($deviceBreakpoint) {
@if $deviceBreakpoint==mobileBreakpoint {
@media screen and (min-width: $breakpoint-mobile) {
@content;
}
} @else if $deviceBreakpoint==tabletBreakpoint {
@media screen and (min-width: $breakpoint-tablet) {
@content;
}
}
}
+3 -26
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import './Client.scss';
import { initHotkeys } from '../../../client/event/hotkeys';
@@ -23,29 +23,6 @@ function Client() {
const [isLoading, changeLoading] = useState(true);
const [loadingMsg, setLoadingMsg] = useState('Heating up');
const [dragCounter, setDragCounter] = useState(0);
const classNameHidden = 'client__item-hidden';
const navWrapperRef = useRef(null);
const roomWrapperRef = useRef(null);
function onRoomSelected() {
navWrapperRef.current?.classList.add(classNameHidden);
roomWrapperRef.current?.classList.remove(classNameHidden);
}
function onNavigationSelected() {
navWrapperRef.current?.classList.remove(classNameHidden);
roomWrapperRef.current?.classList.add(classNameHidden);
}
useEffect(() => {
navigation.on(cons.events.navigation.ROOM_SELECTED, onRoomSelected);
navigation.on(cons.events.navigation.NAVIGATION_OPENED, onNavigationSelected);
return (() => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, onRoomSelected);
navigation.removeListener(cons.events.navigation.NAVIGATION_OPENED, onNavigationSelected);
});
}, []);
useEffect(() => {
let counter = 0;
@@ -151,10 +128,10 @@ function Client() {
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<div className="navigation__wrapper" ref={navWrapperRef}>
<div className="navigation__wrapper">
<Navigation />
</div>
<div className={`room__wrapper ${classNameHidden}`} ref={roomWrapperRef}>
<div className="room__wrapper">
<Room />
</div>
<Windows />
+1 -13
View File
@@ -1,5 +1,3 @@
@use '../../partials/screen';
.client-container {
display: flex;
height: 100%;
@@ -7,22 +5,12 @@
.navigation__wrapper {
width: var(--navigation-width);
@include screen.smallerThan(mobileBreakpoint) {
width: 100%;
}
}
.room__wrapper {
flex: 1;
min-width: 0;
}
@include screen.smallerThan(mobileBreakpoint) {
.client__item-hidden {
display: none;
}
}
.loading-display {
position: absolute;
@@ -53,4 +41,4 @@
.text {
color: var(--tc-link);
}
}
}
-16
View File
@@ -23,13 +23,6 @@ export function selectRoom(roomId, eventId) {
});
}
// Open navigation on compact screen sizes
export function openNavigation() {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_NAVIGATION,
});
}
export function openSpaceSettings(roomId, tabText) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SPACE_SETTINGS,
@@ -157,12 +150,3 @@ export function openReusableContextMenu(placement, cords, render, afterClose) {
afterClose,
});
}
export function openReusableDialog(title, render, afterClose) {
appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_REUSABLE_DIALOG,
title,
render,
afterClose,
});
}
-2
View File
@@ -7,7 +7,6 @@ import RoomList from './state/RoomList';
import AccountData from './state/AccountData';
import RoomsInput from './state/RoomsInput';
import Notifications from './state/Notifications';
import { cryptoCallbacks } from './state/secretStorageKeys';
global.Olm = require('@matrix-org/olm');
@@ -37,7 +36,6 @@ class InitMatrix extends EventEmitter {
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
deviceId: secret.deviceId,
timelineSupport: true,
cryptoCallbacks,
});
await this.matrixClient.initCrypto();
+1 -6
View File
@@ -83,12 +83,7 @@ class Notifications extends EventEmitter {
getNotiType(roomId) {
const mx = this.matrixClient;
let pushRule;
try {
pushRule = mx.getRoomPushRule('global', roomId);
} catch {
pushRule = undefined;
}
const pushRule = mx.getRoomPushRule('global', roomId);
if (pushRule === undefined) {
const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
+1 -1
View File
@@ -206,7 +206,7 @@ class RoomList extends EventEmitter {
this.matrixClient.getRooms().forEach((room) => {
const { roomId } = room;
const tombstone = room.currentState.events.get('m.room.tombstone');
if (tombstone?.get('') !== undefined) {
if (typeof tombstone !== 'undefined') {
const repRoomId = tombstone.get('').getContent().replacement_room;
const repRoomMembership = this.matrixClient.getRoom(repRoomId)?.getMyMembership();
if (repRoomMembership === 'join') return;
-4
View File
@@ -47,8 +47,6 @@ const cons = {
CLICK_REPLY_TO: 'CLICK_REPLY_TO',
OPEN_SEARCH: 'OPEN_SEARCH',
OPEN_REUSABLE_CONTEXT_MENU: 'OPEN_REUSABLE_CONTEXT_MENU',
OPEN_NAVIGATION: 'OPEN_NAVIGATION',
OPEN_REUSABLE_DIALOG: 'OPEN_REUSABLE_DIALOG',
},
room: {
JOIN: 'JOIN',
@@ -94,8 +92,6 @@ const cons = {
REPLY_TO_CLICKED: 'REPLY_TO_CLICKED',
SEARCH_OPENED: 'SEARCH_OPENED',
REUSABLE_CONTEXT_MENU_OPENED: 'REUSABLE_CONTEXT_MENU_OPENED',
NAVIGATION_OPENED: 'NAVIGATION_OPENED',
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
},
roomList: {
ROOMLIST_UPDATED: 'ROOMLIST_UPDATED',
-11
View File
@@ -131,9 +131,6 @@ class Navigation extends EventEmitter {
[cons.actions.navigation.OPEN_SETTINGS]: () => {
this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
},
[cons.actions.navigation.OPEN_NAVIGATION]: () => {
this.emit(cons.events.navigation.NAVIGATION_OPENED);
},
[cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
this.emit(
cons.events.navigation.EMOJIBOARD_OPENED,
@@ -177,14 +174,6 @@ class Navigation extends EventEmitter {
action.afterClose,
);
},
[cons.actions.navigation.OPEN_REUSABLE_DIALOG]: () => {
this.emit(
cons.events.navigation.REUSABLE_DIALOG_OPENED,
action.title,
action.render,
action.afterClose,
);
},
};
actions[action.type]?.();
}
-41
View File
@@ -1,41 +0,0 @@
const secretStorageKeys = new Map();
export function storePrivateKey(keyId, privateKey) {
if (privateKey instanceof Uint8Array === false) {
throw new Error('Unable to store, privateKey is invalid.');
}
secretStorageKeys.set(keyId, privateKey);
}
export function hasPrivateKey(keyId) {
return secretStorageKeys.get(keyId) instanceof Uint8Array;
}
export function getPrivateKey(keyId) {
return secretStorageKeys.get(keyId);
}
export function deletePrivateKey(keyId) {
delete secretStorageKeys.delete(keyId);
}
export function clearSecretStorageKeys() {
secretStorageKeys.clear();
}
async function getSecretStorageKey({ keys }) {
const keyIds = Object.keys(keys);
const keyId = keyIds.find(hasPrivateKey);
if (!keyId) return undefined;
const privateKey = getPrivateKey(keyId);
return [keyId, privateKey];
}
function cacheSecretStorageKey(keyId, keyInfo, privateKey) {
secretStorageKeys.set(keyId, privateKey);
}
export const cryptoCallbacks = {
getSecretStorageKey,
cacheSecretStorageKey,
};
+1 -7
View File
@@ -1,5 +1,3 @@
@use './app/partials/screen';
:root {
/* background color | --bg-[background type]: value */
@@ -71,13 +69,9 @@
--ic-surface-high: #272727;
--ic-surface-normal: #626262;
--ic-surface-low: #7c7c7c;
--ic-primary-high: #ffffff;
--ic-primary-normal: #ffffff;
--ic-positive-high: rgba(69, 184, 59);
--ic-positive-normal: rgba(69, 184, 59, 80%);
--ic-caution-high: rgba(255, 179, 0);
--ic-caution-normal: rgba(255, 179, 0, 80%);
--ic-danger-high: rgba(240, 71, 71);
--ic-danger-normal: rgba(240, 71, 71, 0.7);
/* user mxid colors */
@@ -181,7 +175,7 @@
--popup-window-drawer-width: 280px;
@include screen.smallerThan(tabletBreakpoint) {
@media (max-width: 1124px) {
--navigation-drawer-width: calc(240px + var(--border-width));
--people-drawer-width: calc(256px - var(--border-width));
--popup-window-drawer-width: 240px;
-18
View File
@@ -114,21 +114,3 @@ export function getScrollInfo(target) {
export function avatarInitials(text) {
return [...text][0];
}
export function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
const host = document.body;
const copyInput = document.createElement('input');
copyInput.style.position = 'fixed';
copyInput.style.opacity = '0';
copyInput.value = text;
host.append(copyInput);
copyInput.select();
copyInput.setSelectionRange(0, 99999);
document.execCommand('Copy');
copyInput.remove();
}
}
-36
View File
@@ -162,39 +162,3 @@ export function genRoomVia(room) {
}
return via.concat(mostPop3.slice(0, 2));
}
export function isCrossVerified(deviceId) {
try {
const mx = initMatrix.matrixClient;
const crossSignInfo = mx.getStoredCrossSigningForUser(mx.getUserId());
const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId);
const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true);
return deviceTrust.isCrossSigningVerified();
} catch {
return false;
}
}
export function hasCrossSigningAccountData() {
const mx = initMatrix.matrixClient;
const masterKeyData = mx.getAccountData('m.cross_signing.master');
return !!masterKeyData;
}
export function getDefaultSSKey() {
const mx = initMatrix.matrixClient;
try {
return mx.getAccountData('m.secret_storage.default_key').getContent().key;
} catch {
return undefined;
}
}
export function getSSKeyInfo(key) {
const mx = initMatrix.matrixClient;
try {
return mx.getAccountData(`m.secret_storage.key.${key}`).getContent();
} catch {
return undefined;
}
}
+2 -2
View File
@@ -13,8 +13,8 @@ export function roomIdByAtoZ(aId, bId) {
// remove "#" from the room name
// To ignore it in sorting
aName = aName.replace(/#/g, '');
bName = bName.replace(/#/g, '');
aName = aName.replaceAll('#', '');
bName = bName.replaceAll('#', '');
if (aName.toLowerCase() < bName.toLowerCase()) {
return -1;
-5
View File
@@ -1,7 +1,6 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");
const webpack = require('webpack');
module.exports = {
entry: {
@@ -18,7 +17,6 @@ module.exports = {
'util': require.resolve('util/'),
'assert': require.resolve('assert/'),
'url': require.resolve('url/'),
'buffer': require.resolve('buffer'),
}
},
node: {
@@ -75,8 +73,5 @@ module.exports = {
{ from: 'config.json' },
],
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
],
};