Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ce6cfd487 | ||
|
|
9a22b25564 | ||
|
|
44c3dec9dc | ||
|
|
87f3afd8fd | ||
|
|
53f1129242 | ||
|
|
74216f75e2 | ||
|
|
ed8eca0c1d | ||
|
|
dc8e6e53c7 | ||
|
|
989ab5a432 | ||
|
|
ec26c03d58 | ||
|
|
3b1b3387e7 | ||
|
|
62c03d1334 | ||
|
|
51e12184d7 | ||
|
|
8c01eb9c00 | ||
|
|
bf264d5add | ||
|
|
8b3bd38bad |
No files matched your search
@@ -11,7 +11,7 @@ jobs:
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
uses: actions/checkout@v3.0.1
|
||||
- name: Build app
|
||||
run: npm ci && npm run build
|
||||
- name: Upload artifact
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE3_ID }}
|
||||
timeout-minutes: 1
|
||||
- name: Edit PR Description
|
||||
uses: velas/pr-description@v1.0.1
|
||||
uses: Beakyn/gha-comment-pull-request@v1.0.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
@@ -2,8 +2,9 @@ name: 'Docker check'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- '.github/workflows/docker-pr.yml'
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
@@ -12,7 +13,7 @@ jobs:
|
||||
PR_NUMBER: ${{github.event.number}}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
uses: actions/checkout@v3.0.1
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v2.10.0
|
||||
with:
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
uses: actions/checkout@v3.0.1
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
||||
with:
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
uses: actions/checkout@v3.0.1
|
||||
- name: Build and deploy to Netlify
|
||||
uses: jsmrcaga/action-netlify-deploy@v1.7.2
|
||||
with:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.0.0
|
||||
uses: actions/checkout@v3.0.1
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1.14.1
|
||||
with:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
## Builder
|
||||
FROM node:17.8.0-alpine3.15 as builder
|
||||
FROM node:17.9.0-alpine3.15 as builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
|
||||
Generated
+23
-23
@@ -20,7 +20,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"flux": "^4.0.3",
|
||||
"formik": "^2.2.9",
|
||||
"html-react-parser": "^1.4.9",
|
||||
"html-react-parser": "^1.4.11",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"matrix-js-sdk": "^17.0.0",
|
||||
"micromark": "^3.0.10",
|
||||
@@ -58,7 +58,7 @@
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-loader": "^3.1.0",
|
||||
@@ -5828,9 +5828,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz",
|
||||
"integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz",
|
||||
"integrity": "sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -7199,9 +7199,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/html-dom-parser": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.1.1.tgz",
|
||||
"integrity": "sha512-ddijSO0GluZEdlLNmiAjFI+CkAgzUgf8n3vpitQhvlsniPMbCSWtu+QYmioaVmEN0stytm9C38p6SsTDthv0cA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.2.0.tgz",
|
||||
"integrity": "sha512-2HIpFMvvffsXHFUFjso0M9LqM+1Lm22BF+Df2ba+7QHJXjk63pWChEnI6YG27eaWqUdfnh5/Vy+OXrNTtepRsg==",
|
||||
"dependencies": {
|
||||
"domhandler": "4.3.1",
|
||||
"htmlparser2": "7.2.0"
|
||||
@@ -7284,17 +7284,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/html-react-parser": {
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.9.tgz",
|
||||
"integrity": "sha512-V45RVd7GEGtk5bmL9iBV/AkaTE6jj9uhvUrJ/cWlq06DrcMzvz2kqMeZfWEV2pIWkRDzLAqrotCfmFSvdTjpHA==",
|
||||
"version": "1.4.11",
|
||||
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.11.tgz",
|
||||
"integrity": "sha512-NDSz8MS89I286N7FzNScBtCRBs6MU7w72k+jWk4dBqcTgTrCQHHrIUlryJB9U5MJuKhq5/CFGz0JPPYS6OnjZw==",
|
||||
"dependencies": {
|
||||
"domhandler": "4.3.1",
|
||||
"html-dom-parser": "1.1.1",
|
||||
"html-dom-parser": "1.2.0",
|
||||
"react-property": "2.0.0",
|
||||
"style-to-js": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "0.14 || 15 || 16 || 17"
|
||||
"react": "0.14 || 15 || 16 || 17 || 18"
|
||||
}
|
||||
},
|
||||
"node_modules/html-webpack-plugin": {
|
||||
@@ -18505,9 +18505,9 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-react-hooks": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz",
|
||||
"integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==",
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.4.0.tgz",
|
||||
"integrity": "sha512-U3RVIfdzJaeKDQKEJbz5p3NW8/L80PCATJAfuojwbaEL+gBjfGdhUcGde+WGUW46Q5sr/NgxevsIiDtNXrvZaQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -19484,9 +19484,9 @@
|
||||
}
|
||||
},
|
||||
"html-dom-parser": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.1.1.tgz",
|
||||
"integrity": "sha512-ddijSO0GluZEdlLNmiAjFI+CkAgzUgf8n3vpitQhvlsniPMbCSWtu+QYmioaVmEN0stytm9C38p6SsTDthv0cA==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-1.2.0.tgz",
|
||||
"integrity": "sha512-2HIpFMvvffsXHFUFjso0M9LqM+1Lm22BF+Df2ba+7QHJXjk63pWChEnI6YG27eaWqUdfnh5/Vy+OXrNTtepRsg==",
|
||||
"requires": {
|
||||
"domhandler": "4.3.1",
|
||||
"htmlparser2": "7.2.0"
|
||||
@@ -19542,12 +19542,12 @@
|
||||
}
|
||||
},
|
||||
"html-react-parser": {
|
||||
"version": "1.4.9",
|
||||
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.9.tgz",
|
||||
"integrity": "sha512-V45RVd7GEGtk5bmL9iBV/AkaTE6jj9uhvUrJ/cWlq06DrcMzvz2kqMeZfWEV2pIWkRDzLAqrotCfmFSvdTjpHA==",
|
||||
"version": "1.4.11",
|
||||
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-1.4.11.tgz",
|
||||
"integrity": "sha512-NDSz8MS89I286N7FzNScBtCRBs6MU7w72k+jWk4dBqcTgTrCQHHrIUlryJB9U5MJuKhq5/CFGz0JPPYS6OnjZw==",
|
||||
"requires": {
|
||||
"domhandler": "4.3.1",
|
||||
"html-dom-parser": "1.1.1",
|
||||
"html-dom-parser": "1.2.0",
|
||||
"react-property": "2.0.0",
|
||||
"style-to-js": "1.1.0"
|
||||
}
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"flux": "^4.0.3",
|
||||
"formik": "^2.2.9",
|
||||
"html-react-parser": "^1.4.9",
|
||||
"html-react-parser": "^1.4.11",
|
||||
"linkifyjs": "^2.1.9",
|
||||
"matrix-js-sdk": "^17.0.0",
|
||||
"micromark": "^3.0.10",
|
||||
@@ -64,7 +64,7 @@
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"eslint-plugin-react-hooks": "^4.4.0",
|
||||
"favicons": "^6.2.2",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-loader": "^3.1.0",
|
||||
|
||||
@@ -11,11 +11,12 @@ 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={`ic-btn ic-btn-${variant} ${className}`}
|
||||
onMouseUp={(e) => blurOnBubbling(e, `.ic-btn-${variant}`)}
|
||||
onClick={onClick}
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
@@ -47,6 +48,7 @@ IconButton.defaultProps = {
|
||||
tabIndex: 0,
|
||||
disabled: false,
|
||||
isImage: false,
|
||||
className: '',
|
||||
};
|
||||
|
||||
IconButton.propTypes = {
|
||||
@@ -60,6 +62,7 @@ IconButton.propTypes = {
|
||||
tabIndex: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
isImage: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default IconButton;
|
||||
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
@@ -0,0 +1,79 @@
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* 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;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
.confirm-dialog {
|
||||
padding: var(--sp-normal);
|
||||
|
||||
& > .text {
|
||||
padding-bottom: var(--sp-normal);
|
||||
}
|
||||
&__btn {
|
||||
display: flex;
|
||||
gap: var(--sp-normal);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function Dialog({
|
||||
}) {
|
||||
return (
|
||||
<RawModal
|
||||
className={`${className === null ? '' : `${className} `}dialog-model`}
|
||||
className={`${className === null ? '' : `${className} `}dialog-modal`}
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={onAfterOpen}
|
||||
onAfterClose={onAfterClose}
|
||||
@@ -37,7 +37,7 @@ function Dialog({
|
||||
{contentOptions}
|
||||
</Header>
|
||||
<div className="dialog__content__wrapper">
|
||||
<ScrollView autoHide invisible={invisibleScroll}>
|
||||
<ScrollView autoHide={!invisibleScroll} invisible={invisibleScroll}>
|
||||
<div className="dialog__content-container">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.dialog-model {
|
||||
.dialog-modal {
|
||||
--modal-height: 656px;
|
||||
max-height: min(100%, var(--modal-height));
|
||||
display: flex;
|
||||
|
||||
@@ -24,7 +24,7 @@ function ReusableDialog() {
|
||||
}, []);
|
||||
|
||||
const handleAfterClose = () => {
|
||||
data.afterClose();
|
||||
data.afterClose?.();
|
||||
setData(null);
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ 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">
|
||||
@@ -546,10 +548,15 @@ const MessageOptions = React.memo(({
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
iconSrc={BinIC}
|
||||
onClick={() => {
|
||||
if (window.confirm('Are you sure you want to delete this event')) {
|
||||
redactEvent(roomId, mEvent.getId());
|
||||
}
|
||||
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());
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@use '../../atoms/scroll/scrollbar';
|
||||
@use '../../partials/text';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.message,
|
||||
.ph-msg {
|
||||
@@ -95,7 +96,7 @@
|
||||
.message__reactions {
|
||||
max-width: calc(100% - 88px);
|
||||
min-width: 0;
|
||||
@media (max-width: 1124px) {
|
||||
@include screen.smallerThan(tabletBreakpoint) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ function PopupWindow({
|
||||
|
||||
return (
|
||||
<RawModal
|
||||
className={`${className === null ? '' : `${className} `}pw-model`}
|
||||
className={`${className === null ? '' : `${className} `}pw-modal`}
|
||||
overlayClassName="pw-modal__overlay"
|
||||
isOpen={isOpen}
|
||||
onAfterClose={onAfterClose}
|
||||
onRequestClose={onRequestClose}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.pw-model {
|
||||
.pw-modal {
|
||||
--modal-height: 774px;
|
||||
max-height: var(--modal-height) !important;
|
||||
height: 100%;
|
||||
|
||||
@include screen.smallerThan(mobileBreakpoint) {
|
||||
--modal-height: 100%;
|
||||
border-radius: 0 !important;
|
||||
&__overlay {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pw {
|
||||
@@ -72,4 +81,4 @@
|
||||
@include dir.side(margin, 0, var(--sp-tight));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ 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);
|
||||
@@ -15,17 +17,20 @@ function RoomEncryption({ roomId }) {
|
||||
const [isEncrypted, setIsEncrypted] = useState(encryptionEvents.length > 0);
|
||||
const canEnableEncryption = room.currentState.maySendStateEvent('m.room.encryption', mx.getUserId());
|
||||
|
||||
const handleEncryptionEnable = () => {
|
||||
const handleEncryptionEnable = async () => {
|
||||
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';
|
||||
if (joinRule === 'public' ? confirm(confirmMsg1) : true) {
|
||||
if (confirm(confirmMsg2)) {
|
||||
setIsEncrypted(true);
|
||||
mx.sendStateEvent(roomId, 'm.room.encryption', {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
});
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ 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);
|
||||
@@ -29,11 +31,16 @@ function RoomOptions({ roomId, afterOptionSelect }) {
|
||||
openInviteUser(roomId);
|
||||
afterOptionSelect();
|
||||
};
|
||||
const handleLeaveClick = () => {
|
||||
if (confirm('Are you really want to leave this room?')) {
|
||||
roomActions.leave(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);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,6 +19,7 @@ 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();
|
||||
@@ -117,7 +118,13 @@ function RoomProfile({ roomId }) {
|
||||
|
||||
const handleAvatarUpload = async (url) => {
|
||||
if (url === null) {
|
||||
if (confirm('Are you sure you want to remove avatar?')) {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Remove avatar',
|
||||
'Are you sure that you want to remove room avatar?',
|
||||
'Remove',
|
||||
'caution',
|
||||
);
|
||||
if (isConfirmed) {
|
||||
await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
}
|
||||
} else await mx.sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||
|
||||
@@ -3,7 +3,6 @@ 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,12 @@ import Tooltip from '../../atoms/tooltip/Tooltip';
|
||||
import { blurOnBubbling } from '../../atoms/button/script';
|
||||
|
||||
const SidebarAvatar = React.forwardRef(({
|
||||
tooltip, active, onClick, onContextMenu,
|
||||
avatar, notificationBadge,
|
||||
className, tooltip, active, onClick,
|
||||
onContextMenu, avatar, notificationBadge,
|
||||
}, ref) => {
|
||||
let activeClass = '';
|
||||
if (active) activeClass = ' sidebar-avatar--active';
|
||||
const classes = ['sidebar-avatar'];
|
||||
if (active) classes.push('sidebar-avatar--active');
|
||||
if (className) classes.push(className);
|
||||
return (
|
||||
<Tooltip
|
||||
content={<Text variant="b1">{twemojify(tooltip)}</Text>}
|
||||
@@ -21,7 +22,7 @@ const SidebarAvatar = React.forwardRef(({
|
||||
>
|
||||
<button
|
||||
ref={ref}
|
||||
className={`sidebar-avatar${activeClass}`}
|
||||
className={classes.join(' ')}
|
||||
type="button"
|
||||
onMouseUp={(e) => blurOnBubbling(e, '.sidebar-avatar')}
|
||||
onClick={onClick}
|
||||
@@ -34,6 +35,7 @@ const SidebarAvatar = React.forwardRef(({
|
||||
);
|
||||
});
|
||||
SidebarAvatar.defaultProps = {
|
||||
className: null,
|
||||
active: false,
|
||||
onClick: null,
|
||||
onContextMenu: null,
|
||||
@@ -41,6 +43,7 @@ SidebarAvatar.defaultProps = {
|
||||
};
|
||||
|
||||
SidebarAvatar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
tooltip: PropTypes.string.isRequired,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -24,6 +24,8 @@ 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);
|
||||
@@ -54,11 +56,16 @@ function SpaceOptions({ roomId, afterOptionSelect }) {
|
||||
afterOptionSelect();
|
||||
};
|
||||
|
||||
const handleLeaveClick = () => {
|
||||
if (confirm('Are you really want to leave this space?')) {
|
||||
leave(roomId);
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import Text from '../../atoms/text/Text';
|
||||
function DragDrop({ isOpen }) {
|
||||
return (
|
||||
<RawModal
|
||||
className="drag-drop__model"
|
||||
className="drag-drop__modal"
|
||||
overlayClassName="drag-drop__overlay"
|
||||
isOpen={isOpen}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.drag-drop__model {
|
||||
.drag-drop__modal {
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
--emoji-board-height: 390px;
|
||||
--emoji-board-width: 286px;
|
||||
display: flex;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
|
||||
&__content {
|
||||
@extend .cp-fx__item-one;
|
||||
@@ -91,6 +93,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.emoji-group {
|
||||
--emoji-padding: 6px;
|
||||
position: relative;
|
||||
|
||||
@@ -54,17 +54,19 @@ function InviteList({ isOpen, onRequestClose }) {
|
||||
}, [procInvite]);
|
||||
|
||||
function renderRoomTile(roomId) {
|
||||
const myRoom = initMatrix.matrixClient.getRoom(roomId);
|
||||
const mx = initMatrix.matrixClient;
|
||||
const myRoom = mx.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={myRoom.getJoinedMembers()[0].userId}
|
||||
inviterName={inviterName}
|
||||
options={
|
||||
procInvite.has(myRoom.roomId)
|
||||
? (<Spinner size="small" />)
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
@@ -26,8 +27,12 @@ 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;
|
||||
@@ -85,6 +90,22 @@ 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();
|
||||
@@ -358,6 +379,7 @@ function SideBar() {
|
||||
notificationBadge={<NotificationBadge alert content={totalInvites} />}
|
||||
/>
|
||||
)}
|
||||
<CrossSigninAlert />
|
||||
<ProfileAvatarMenu />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,4 +57,21 @@
|
||||
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,6 +12,7 @@ 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';
|
||||
|
||||
@@ -38,9 +39,15 @@ function ProfileEditor({ userId }) {
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
const handleAvatarUpload = (url) => {
|
||||
const handleAvatarUpload = async (url) => {
|
||||
if (url === null) {
|
||||
if (confirm('Are you sure you want to remove avatar?')) {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Remove avatar',
|
||||
'Are you sure that you want to remove avatar?',
|
||||
'Remove',
|
||||
'caution',
|
||||
);
|
||||
if (isConfirmed) {
|
||||
mx.setAvatarUrl('');
|
||||
setAvatarSrc(null);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ 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,
|
||||
@@ -362,7 +363,7 @@ function ProfileViewer() {
|
||||
&& (powerLevel < myPowerLevel || userId === mx.getUserId())
|
||||
);
|
||||
|
||||
const handleChangePowerLevel = (newPowerLevel) => {
|
||||
const handleChangePowerLevel = async (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?';
|
||||
@@ -370,9 +371,14 @@ function ProfileViewer() {
|
||||
const isSharedPower = newPowerLevel === myPowerLevel;
|
||||
const isDemotingMyself = userId === mx.getUserId();
|
||||
if (isSharedPower || isDemotingMyself) {
|
||||
if (confirm(isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG)) {
|
||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||
}
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Change power level',
|
||||
isSharedPower ? SHARED_POWER_MSG : DEMOTING_MYSELF_MSG,
|
||||
'Change',
|
||||
'caution',
|
||||
);
|
||||
if (!isConfirmed) return;
|
||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||
} else {
|
||||
roomActions.setPowerLevel(roomId, userId, newPowerLevel);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ 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';
|
||||
@@ -53,7 +54,10 @@ function Room() {
|
||||
}, []);
|
||||
|
||||
const { roomTimeline, eventId } = roomInfo;
|
||||
if (roomTimeline === null) return <Welcome />;
|
||||
if (roomTimeline === null) {
|
||||
setTimeout(() => openNavigation());
|
||||
return <Welcome />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="room">
|
||||
@@ -61,7 +65,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,4 +1,5 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.room {
|
||||
@extend .cp-fx__row;
|
||||
@@ -9,4 +10,10 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.room .people-drawer {
|
||||
@include screen.smallerThan(tabletBreakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ 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',
|
||||
@@ -85,10 +86,15 @@ function GeneralSettings({ roomId }) {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (confirm('Are you really want to leave this room?')) {
|
||||
roomActions.leave(roomId);
|
||||
}
|
||||
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);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/screen';
|
||||
@use '../../partials/dir';
|
||||
|
||||
.room-view {
|
||||
@extend .cp-fx__column;
|
||||
@@ -18,6 +20,12 @@
|
||||
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;
|
||||
|
||||
@@ -44,7 +44,9 @@ const commands = [{
|
||||
}, {
|
||||
name: 'leave',
|
||||
description: 'Leave current room',
|
||||
exe: (roomId) => roomActions.leave(roomId),
|
||||
exe: (roomId) => {
|
||||
roomActions.leave(roomId);
|
||||
},
|
||||
}, {
|
||||
name: 'invite',
|
||||
isOptions: true,
|
||||
|
||||
@@ -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 } from '../../../client/action/navigation';
|
||||
import { toggleRoomSettings, openReusableContextMenu, openNavigation } from '../../../client/action/navigation';
|
||||
import { togglePeopleDrawer } from '../../../client/action/settings';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { getEventCords } from '../../../util/common';
|
||||
@@ -25,6 +25,7 @@ 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';
|
||||
|
||||
@@ -73,6 +74,12 @@ 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"
|
||||
@@ -87,7 +94,8 @@ function RoomViewHeader({ roomId }) {
|
||||
<RawIcon src={ChevronBottomIC} />
|
||||
</button>
|
||||
<IconButton onClick={() => toggleRoomSettings(tabText.SEARCH)} tooltip="Search" src={SearchIC} />
|
||||
<IconButton onClick={togglePeopleDrawer} tooltip="People" src={UserIC} />
|
||||
<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={openRoomOptions}
|
||||
tooltip="Options"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.room-header__btn {
|
||||
min-width: 0;
|
||||
@@ -24,4 +25,23 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,7 @@ function Search() {
|
||||
|
||||
return (
|
||||
<RawModal
|
||||
className="search-dialog__model dialog-model"
|
||||
className="search-dialog__modal dialog-modal"
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={handleAfterOpen}
|
||||
onAfterClose={handleAfterClose}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use '../../partials/dir';
|
||||
|
||||
.search-dialog__model {
|
||||
.search-dialog__modal {
|
||||
--modal-height: 380px;
|
||||
height: 100%;
|
||||
background-color: var(--bg-surface);
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
@@ -0,0 +1,12 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/* 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;
|
||||
@@ -0,0 +1,55 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -3,62 +3,67 @@ 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';
|
||||
|
||||
function useDeviceList() {
|
||||
const mx = initMatrix.matrixClient;
|
||||
const [deviceList, setDeviceList] = useState(null);
|
||||
const promptDeviceName = async (deviceName) => new Promise((resolve) => {
|
||||
let isCompleted = false;
|
||||
|
||||
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();
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
mx.on('crypto.devicesUpdated', handleDevicesUpdate);
|
||||
return () => {
|
||||
mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate);
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
return deviceList;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
@@ -91,7 +96,7 @@ function DeviceManage() {
|
||||
}
|
||||
|
||||
const handleRename = async (device) => {
|
||||
const newName = window.prompt('Edit session name', device.display_name);
|
||||
const newName = await promptDeviceName(device.display_name);
|
||||
if (newName === null || newName.trim() === '') return;
|
||||
if (newName.trim() === device.display_name) return;
|
||||
addToProcessing(device);
|
||||
@@ -105,39 +110,21 @@ function DeviceManage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async (device, auth = undefined) => {
|
||||
if (auth === undefined
|
||||
? window.confirm(`You are about to logout "${device.display_name}" session?`)
|
||||
: true
|
||||
) {
|
||||
addToProcessing(device);
|
||||
try {
|
||||
await mx.deleteDevice(device.device_id, auth);
|
||||
} catch (e) {
|
||||
if (e.httpStatus === 401 && e.data?.flows) {
|
||||
const { flows } = e.data;
|
||||
const flow = flows.find((f) => f.stages.includes('m.login.password'));
|
||||
if (flow) {
|
||||
const password = window.prompt('Please enter account password', '');
|
||||
if (password && password.trim() !== '') {
|
||||
handleRemove(device, {
|
||||
session: e.data.session,
|
||||
type: 'm.login.password',
|
||||
password,
|
||||
identifier: {
|
||||
type: 'm.id.user',
|
||||
user: mx.getUserId(),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
window.alert('Failed to remove session!');
|
||||
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) => {
|
||||
@@ -187,6 +174,16 @@ function DeviceManage() {
|
||||
<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))
|
||||
|
||||
@@ -15,4 +15,15 @@
|
||||
& .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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/* 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;
|
||||
@@ -0,0 +1,27 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
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;
|
||||
@@ -0,0 +1,20 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ 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';
|
||||
@@ -36,6 +38,7 @@ 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({});
|
||||
@@ -168,18 +171,13 @@ function SecuritySection() {
|
||||
return (
|
||||
<div className="settings-security">
|
||||
<div className="settings-security__card">
|
||||
<MenuHeader>Session Info</MenuHeader>
|
||||
<SettingTile
|
||||
title={`Session ID: ${initMatrix.matrixClient.getDeviceId()}`}
|
||||
/>
|
||||
<SettingTile
|
||||
title={`Session key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
|
||||
content={<Text variant="b3">Use this session ID-key combo to verify or manage this session.</Text>}
|
||||
/>
|
||||
<MenuHeader>Cross signing and backup</MenuHeader>
|
||||
<CrossSigning />
|
||||
<KeyBackup />
|
||||
</div>
|
||||
<DeviceManage />
|
||||
<div className="settings-security__card">
|
||||
<MenuHeader>Encryption</MenuHeader>
|
||||
<MenuHeader>Export/Import encryption keys</MenuHeader>
|
||||
<SettingTile
|
||||
title="Export E2E room keys"
|
||||
content={(
|
||||
@@ -247,7 +245,7 @@ function AboutSection() {
|
||||
);
|
||||
}
|
||||
|
||||
const tabText = {
|
||||
export const tabText = {
|
||||
APPEARANCE: 'Appearance',
|
||||
NOTIFICATIONS: 'Notifications',
|
||||
SECURITY: 'Security',
|
||||
@@ -300,8 +298,10 @@ function Settings() {
|
||||
const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
|
||||
|
||||
const handleTabChange = (tabItem) => setSelectedTab(tabItem);
|
||||
const handleLogout = () => {
|
||||
if (confirm('Confirm logout')) logout();
|
||||
const handleLogout = async () => {
|
||||
if (await confirmDialog('Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger')) {
|
||||
logout();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use '../../partials/flex';
|
||||
@use '../../partials/dir';
|
||||
@use '../../partials/screen';
|
||||
|
||||
.settings-window {
|
||||
& .pw {
|
||||
@@ -86,4 +87,4 @@
|
||||
margin: var(--sp-extra-tight) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ 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 = {
|
||||
@@ -61,6 +62,7 @@ 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 (
|
||||
@@ -89,10 +91,14 @@ function GeneralSettings({ roomId }) {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
if (confirm('Are you really want to leave this space?')) {
|
||||
leave(roomId);
|
||||
}
|
||||
onClick={async () => {
|
||||
const isConfirmed = await confirmDialog(
|
||||
'Leave space',
|
||||
`Are you sure that you want to leave "${roomName}" space?`,
|
||||
'Leave',
|
||||
'danger',
|
||||
);
|
||||
if (isConfirmed) leave(roomId);
|
||||
}}
|
||||
iconSrc={LeaveArrowIC}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import './Client.scss';
|
||||
|
||||
import { initHotkeys } from '../../../client/event/hotkeys';
|
||||
@@ -23,6 +23,29 @@ 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;
|
||||
@@ -128,10 +151,10 @@ function Client() {
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="navigation__wrapper">
|
||||
<div className="navigation__wrapper" ref={navWrapperRef}>
|
||||
<Navigation />
|
||||
</div>
|
||||
<div className="room__wrapper">
|
||||
<div className={`room__wrapper ${classNameHidden}`} ref={roomWrapperRef}>
|
||||
<Room />
|
||||
</div>
|
||||
<Windows />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '../../partials/screen';
|
||||
|
||||
.client-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
@@ -5,12 +7,22 @@
|
||||
|
||||
.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;
|
||||
@@ -41,4 +53,4 @@
|
||||
.text {
|
||||
color: var(--tc-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,13 @@ 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,
|
||||
|
||||
@@ -7,6 +7,7 @@ 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');
|
||||
|
||||
@@ -36,6 +37,7 @@ class InitMatrix extends EventEmitter {
|
||||
cryptoStore: new sdk.IndexedDBCryptoStore(global.indexedDB, 'crypto-store'),
|
||||
deviceId: secret.deviceId,
|
||||
timelineSupport: true,
|
||||
cryptoCallbacks,
|
||||
});
|
||||
|
||||
await this.matrixClient.initCrypto();
|
||||
|
||||
@@ -47,6 +47,7 @@ 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: {
|
||||
@@ -93,6 +94,7 @@ 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: {
|
||||
|
||||
@@ -131,6 +131,9 @@ 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,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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,
|
||||
};
|
||||
+7
-1
@@ -1,3 +1,5 @@
|
||||
@use './app/partials/screen';
|
||||
|
||||
:root {
|
||||
|
||||
/* background color | --bg-[background type]: value */
|
||||
@@ -69,9 +71,13 @@
|
||||
--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 */
|
||||
@@ -175,7 +181,7 @@
|
||||
|
||||
--popup-window-drawer-width: 280px;
|
||||
|
||||
@media (max-width: 1124px) {
|
||||
@include screen.smallerThan(tabletBreakpoint) {
|
||||
--navigation-drawer-width: calc(240px + var(--border-width));
|
||||
--people-drawer-width: calc(256px - var(--border-width));
|
||||
--popup-window-drawer-width: 240px;
|
||||
|
||||
@@ -114,3 +114,21 @@ 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();
|
||||
}
|
||||
}
|
||||
@@ -162,3 +162,39 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
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: {
|
||||
@@ -17,6 +18,7 @@ module.exports = {
|
||||
'util': require.resolve('util/'),
|
||||
'assert': require.resolve('assert/'),
|
||||
'url': require.resolve('url/'),
|
||||
'buffer': require.resolve('buffer'),
|
||||
}
|
||||
},
|
||||
node: {
|
||||
@@ -73,5 +75,8 @@ module.exports = {
|
||||
{ from: 'config.json' },
|
||||
],
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
],
|
||||
};
|
||||
Reference in new issue
Block a user