mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 19:56:47 +03:00
Merge pull request #3620 from matrix-org/bwindels/userinfomakeover
New design for member panel
This commit is contained in:
commit
41f832a549
18 changed files with 808 additions and 455 deletions
|
@ -90,6 +90,7 @@
|
|||
@import "./views/elements/_ErrorBoundary.scss";
|
||||
@import "./views/elements/_EventListSummary.scss";
|
||||
@import "./views/elements/_Field.scss";
|
||||
@import "./views/elements/_IconButton.scss";
|
||||
@import "./views/elements/_ImageView.scss";
|
||||
@import "./views/elements/_InlineSpinner.scss";
|
||||
@import "./views/elements/_InteractiveTooltip.scss";
|
||||
|
|
|
@ -49,6 +49,7 @@ limitations under the License.
|
|||
color: $primary-fg-color;
|
||||
background-color: $primary-bg-color;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mx_Field select {
|
||||
|
|
55
res/css/views/elements/_IconButton.scss
Normal file
55
res/css/views/elements/_IconButton.scss
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_IconButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 100%;
|
||||
background-color: $accent-bg-color;
|
||||
// don't shrink or grow if in a flex container
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.mx_AccessibleButton_disabled {
|
||||
background-color: none;
|
||||
|
||||
&::before {
|
||||
background-color: lightgrey;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 90%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 55%;
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
&.mx_IconButton_icon_check::before {
|
||||
mask-image: url('$(res)/img/feather-customised/check.svg');
|
||||
}
|
||||
|
||||
&.mx_IconButton_icon_edit::before {
|
||||
mask-image: url('$(res)/img/feather-customised/edit.svg');
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ limitations under the License.
|
|||
width: 12px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
mask: url("$(res)/img/e2e/verified.svg");
|
||||
mask: url("$(res)/img/e2e/normal.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100%;
|
||||
margin-top: 4px;
|
||||
|
@ -33,6 +33,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
&.mx_KeyVerification_icon_verified::after {
|
||||
mask: url("$(res)/img/e2e/verified.svg");
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -20,14 +20,7 @@ limitations under the License.
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mx_UserInfo_profile .mx_E2EIcon {
|
||||
display: inline;
|
||||
margin: auto;
|
||||
padding-right: 25px;
|
||||
mask-size: contain;
|
||||
}
|
||||
font-size: 12px;
|
||||
|
||||
.mx_UserInfo_cancel {
|
||||
height: 16px;
|
||||
|
@ -38,18 +31,13 @@ limitations under the License.
|
|||
mask-repeat: no-repeat;
|
||||
mask-position: 16px center;
|
||||
background-color: $rightpanel-button-color;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.mx_UserInfo_profile h2 {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.mx_UserInfo h2 {
|
||||
font-size: 16px;
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px 0;
|
||||
margin: 18px 0 0 0;
|
||||
}
|
||||
|
||||
.mx_UserInfo_container {
|
||||
|
@ -61,70 +49,124 @@ limitations under the License.
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.mx_UserInfo .mx_RoomTile_nameContainer {
|
||||
.mx_RoomTile_nameContainer {
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
.mx_UserInfo .mx_RoomTile_badge {
|
||||
.mx_RoomTile_badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_UserInfo .mx_RoomTile_name {
|
||||
.mx_RoomTile_name {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar {
|
||||
background: $tagpanel-bg-color;
|
||||
margin: 24px 32px 0 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar > img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
max-height: 30vh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
.mx_UserInfo_avatar > div {
|
||||
max-width: 30vh;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar > div > div {
|
||||
/* use padding-top instead of height to make this element square,
|
||||
as the % in padding is a % of the width (including margin,
|
||||
that's why we had to put the margin to center on a parent div),
|
||||
and not a % of the parent height. */
|
||||
padding-top: 100%;
|
||||
height: 0;
|
||||
border-radius: 100%;
|
||||
box-sizing: content-box;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.mx_UserInfo h3 {
|
||||
h3 {
|
||||
text-transform: uppercase;
|
||||
color: $input-darker-fg-color;
|
||||
color: $notice-secondary-color;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.mx_UserInfo_profileField {
|
||||
font-size: 15px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.mx_UserInfo_memberDetails {
|
||||
.mx_UserInfo_profile {
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
max-height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.mx_E2EIcon {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_profileStatus {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_memberDetails .mx_UserInfo_profileField {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
margin: 6px 0;
|
||||
|
||||
.mx_IconButton, .mx_Spinner {
|
||||
margin-left: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&::before {
|
||||
mask-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_roleDescription {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// try to make it the same height as the dropdown
|
||||
margin: 11px 0 12px 0;
|
||||
|
||||
.mx_IconButton {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_field {
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
color: $primary-fg-color;
|
||||
margin-left: 8px;
|
||||
line-height: 23px;
|
||||
}
|
||||
color: $accent-color;
|
||||
line-height: 16px;
|
||||
margin: 8px 0;
|
||||
|
||||
.mx_UserInfo_createRoom {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
&.mx_UserInfo_destructive {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
.mx_UserInfo_createRoom_label {
|
||||
width: initial !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_UserInfo_statusMessage {
|
||||
|
@ -134,42 +176,66 @@ limitations under the License.
|
|||
white-space: nowrap;
|
||||
text-overflow: clip;
|
||||
}
|
||||
.mx_UserInfo .mx_UserInfo_scrollContainer {
|
||||
flex: 1;
|
||||
|
||||
.mx_UserInfo_scrollContainer {
|
||||
flex: 1 1 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_UserInfo .mx_UserInfo_scrollContainer .mx_UserInfo_container {
|
||||
.mx_UserInfo_scrollContainer .mx_UserInfo_container {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
|
||||
> :not(h3) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_container_header {
|
||||
.mx_UserInfo_devices {
|
||||
.mx_UserInfo_device {
|
||||
display: flex;
|
||||
|
||||
&.mx_UserInfo_device_verified {
|
||||
.mx_UserInfo_device_trusted {
|
||||
color: $accent-color;
|
||||
}
|
||||
}
|
||||
&.mx_UserInfo_device_unverified {
|
||||
.mx_UserInfo_device_trusted {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_container_header_right {
|
||||
position: relative;
|
||||
margin-left: auto;
|
||||
.mx_UserInfo_device_name {
|
||||
flex: 1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_newDmButton {
|
||||
background-color: $roomheader-addroom-bg-color;
|
||||
border-radius: 10px; // 16/2 + 2 padding
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
// both for icon in expand button and device item
|
||||
.mx_E2EIcon {
|
||||
// don't squeeze
|
||||
flex: 0 0 auto;
|
||||
margin: 2px 5px 0 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background-color: $roomheader-addroom-fg-color;
|
||||
mask: url('$(res)/img/icons-room-add.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
.mx_UserInfo_expand {
|
||||
display: flex;
|
||||
margin-top: 11px;
|
||||
color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_verify {
|
||||
display: block;
|
||||
background-color: $accent-color;
|
||||
color: $accent-fg-color;
|
||||
border-radius: 4px;
|
||||
padding: 7px 1.5em;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,17 +17,56 @@ limitations under the License.
|
|||
.mx_E2EIcon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center 0;
|
||||
margin: 0 9px;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified {
|
||||
mask-image: url('$(res)/img/e2e/lock-verified.svg');
|
||||
.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before {
|
||||
content: "";
|
||||
display: block;
|
||||
/* the symbols in the shield icons are cut out to make it themeable with css masking.
|
||||
if they appear on a different background than white, the symbol wouldn't be white though, so we
|
||||
add a rectangle here below the masked element to shine through the symbol cut-out.
|
||||
hardcoding white and not using a theme variable as this would probably be white for any theme. */
|
||||
background-color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified::after, .mx_E2EIcon_warning::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_verified::before {
|
||||
/* white rectangle below checkmark of shield */
|
||||
margin: 25% 28% 38% 25%;
|
||||
}
|
||||
|
||||
|
||||
.mx_E2EIcon_verified::after {
|
||||
mask-image: url('$(res)/img/e2e/verified.svg');
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_warning {
|
||||
mask-image: url('$(res)/img/e2e/lock-warning.svg');
|
||||
|
||||
.mx_E2EIcon_warning::before {
|
||||
/* white rectangle below "!" of shield */
|
||||
margin: 18% 40% 25% 40%;
|
||||
}
|
||||
|
||||
.mx_E2EIcon_warning::after {
|
||||
mask-image: url('$(res)/img/e2e/warning.svg');
|
||||
background-color: $warning-color;
|
||||
}
|
||||
|
|
|
@ -78,8 +78,11 @@ limitations under the License.
|
|||
.mx_MessageComposer_e2eIcon.mx_E2EIcon {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
|
||||
&::after {
|
||||
background-color: $composer-e2e-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MessageComposer_noperm_error {
|
||||
width: 100%;
|
||||
|
|
3
res/img/e2e/normal.svg
Normal file
3
res/img/e2e/normal.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 21C12 21 21 17.2 21 11.5V4.85L12 2L3 4.85V11.5C3 17.2 12 21 12 21Z" fill="#2E2F32" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 303 B |
|
@ -1,3 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="12" viewBox="0 0 11 12">
|
||||
<path fill="#7AC9A1" fill-rule="evenodd" stroke="#7AC9A1" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M5.5 11S10 9 10 6V2.5L5.5 1 1 2.5V6c0 3 4.5 5 4.5 5z"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none">
|
||||
<path
|
||||
style="stroke:none;fill:#03b381;fill-opacity:1"
|
||||
d="M 12 2 L 3 4.8496094 L 3 11.5 C 3 17.2 12 21 12 21 C 12 21 21 17.2 21 11.5 L 21 4.8496094 L 12 2 z M 16.541016 7.5332031 C 16.789066 7.5332031 17.037312 7.6240256 17.226562 7.8066406 C 17.605062 8.1718706 17.605063 8.7636762 17.226562 9.1289062 L 11.400391 14.75 C 11.021891 15.1152 10.40975 15.1152 10.03125 14.75 L 10.013672 14.734375 C 10.007572 14.728775 10.002044 14.722597 9.9960938 14.716797 L 7.3242188 12.138672 C 6.9267788 11.755172 6.9267788 11.1335 7.3242188 10.75 C 7.7216487 10.3665 8.3662319 10.3665 8.7636719 10.75 L 10.783203 12.699219 L 15.855469 7.8066406 C 16.044719 7.6240256 16.292966 7.5332031 16.541016 7.5332031 z "
|
||||
id="path2" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 276 B After Width: | Height: | Size: 902 B |
|
@ -1,6 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 12 12">
|
||||
<defs>
|
||||
<path id="a" d="M5 10A5 5 0 1 0 5 0a5 5 0 0 0 0 10zM5 .5A1.5 1.5 0 0 1 6.5 2v3a1.5 1.5 0 0 1-3 0V2A1.5 1.5 0 0 1 5 .5zm0 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/>
|
||||
</defs>
|
||||
<use fill="#F56679" fill-rule="evenodd" stroke="#F56679" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" transform="translate(1 1)" xlink:href="#a"/>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
style="fill-opacity:1;fill:#ff4b55;stroke:none"
|
||||
d="M 12 2 L 3 4.8496094 L 3 11.5 C 3 17.2 12 21 12 21 C 12 21 21 17.2 21 11.5 L 21 4.8496094 L 12 2 z M 12.050781 5.5 C 12.743281 5.5 13.300781 6.0575 13.300781 6.75 L 13.300781 12.25 C 13.300781 12.9425 12.743281 13.5 12.050781 13.5 C 11.358281 13.5 10.800781 12.9425 10.800781 12.25 L 10.800781 6.75 C 10.800781 6.0575 11.358281 5.5 12.050781 5.5 z M 12.050781 15 C 12.743281 15 13.300781 15.5575 13.300781 16.25 C 13.300781 16.9425 12.743281 17.5 12.050781 17.5 C 11.358281 17.5 10.800781 16.9425 10.800781 16.25 C 10.800781 15.5575 11.358281 15 12.050781 15 z "
|
||||
id="path2" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 824 B |
4
res/img/feather-customised/edit.svg
Normal file
4
res/img/feather-customised/edit.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 2L18 6L7 17H3V13L14 2V2Z" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 22H21" stroke="#2E2F32" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 333 B |
|
@ -28,8 +28,8 @@ export function levelRoleMap(usersDefault) {
|
|||
export function textualPowerLevel(level, usersDefault) {
|
||||
const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
|
||||
if (LEVEL_ROLE_MAP[level]) {
|
||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
|
||||
return LEVEL_ROLE_MAP[level];
|
||||
} else {
|
||||
return level;
|
||||
return _t("Custom (%(level)s)", {level});
|
||||
}
|
||||
}
|
||||
|
|
34
src/components/views/elements/IconButton.js
Normal file
34
src/components/views/elements/IconButton.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
|
||||
export default function IconButton(props) {
|
||||
const {icon, className, ...restProps} = props;
|
||||
|
||||
let newClassName = (className || "") + " mx_IconButton";
|
||||
newClassName = newClassName + " mx_IconButton_icon_" + icon;
|
||||
|
||||
const allProps = Object.assign({}, restProps, {className: newClassName});
|
||||
|
||||
return React.createElement(AccessibleButton, allProps);
|
||||
}
|
||||
|
||||
IconButton.propTypes = Object.assign({
|
||||
icon: PropTypes.string,
|
||||
}, AccessibleButton.propTypes);
|
|
@ -129,10 +129,11 @@ module.exports = createReactClass({
|
|||
|
||||
render: function() {
|
||||
let picker;
|
||||
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||
if (this.state.custom) {
|
||||
picker = (
|
||||
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
|
||||
label={this.props.label || _t("Power level")} max={this.props.maxValue}
|
||||
label={label} max={this.props.maxValue}
|
||||
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
|
||||
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
||||
);
|
||||
|
@ -151,7 +152,7 @@ module.exports = createReactClass({
|
|||
|
||||
picker = (
|
||||
<Field id={`powerSelector_notCustom_${this.props.powerLevelKey}`} element="select"
|
||||
label={this.props.label || _t("Power level")} onChange={this.onSelectChange}
|
||||
label={label} onChange={this.onSelectChange}
|
||||
value={String(this.state.selectValue)} disabled={this.props.disabled}>
|
||||
{options}
|
||||
</Field>
|
||||
|
|
|
@ -27,7 +27,6 @@ import sdk from '../../../index';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import createRoom from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import Unread from '../../../Unread';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
|||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {textualPowerLevel} from '../../../Roles';
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
|
@ -63,10 +63,92 @@ const _getE2EStatus = (devices) => {
|
|||
return hasUnverifiedDevice ? "warning" : "verified";
|
||||
};
|
||||
|
||||
const DevicesSection = ({devices, userId, loading}) => {
|
||||
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
|
||||
async function unverifyUser(matrixClient, userId) {
|
||||
const devices = await matrixClient.getStoredDevicesForUser(userId);
|
||||
for (const device of devices) {
|
||||
if (device.isVerified()) {
|
||||
matrixClient.setDeviceVerified(
|
||||
userId, device.deviceId, false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openDMForUser(matrixClient, userId) {
|
||||
const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||
const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
if (!room || room.getMyMembership() === "leave") {
|
||||
return lastActiveRoom;
|
||||
}
|
||||
if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) {
|
||||
return room;
|
||||
}
|
||||
return lastActiveRoom;
|
||||
}, null);
|
||||
|
||||
if (lastActiveRoom) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: lastActiveRoom.roomId,
|
||||
});
|
||||
} else {
|
||||
createRoom({dmUserId: userId});
|
||||
}
|
||||
}
|
||||
|
||||
function useIsEncrypted(cli, room) {
|
||||
const [isEncrypted, setIsEncrypted] = useState(cli.isRoomEncrypted(room.roomId));
|
||||
|
||||
const update = useCallback((event) => {
|
||||
if (event.getType() === "m.room.encryption") {
|
||||
setIsEncrypted(cli.isRoomEncrypted(room.roomId));
|
||||
}
|
||||
}, [cli, room]);
|
||||
useEventEmitter(room.currentState, "RoomState.events", update);
|
||||
return isEncrypted;
|
||||
}
|
||||
|
||||
function verifyDevice(userId, device) {
|
||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||
userId: userId,
|
||||
device: device,
|
||||
});
|
||||
}
|
||||
|
||||
function DeviceItem({userId, device}) {
|
||||
const classes = classNames("mx_UserInfo_device", {
|
||||
mx_UserInfo_device_verified: device.isVerified(),
|
||||
mx_UserInfo_device_unverified: !device.isVerified(),
|
||||
});
|
||||
const iconClasses = classNames("mx_E2EIcon", {
|
||||
mx_E2EIcon_verified: device.isVerified(),
|
||||
mx_E2EIcon_warning: !device.isVerified(),
|
||||
});
|
||||
|
||||
const onDeviceClick = () => {
|
||||
if (!device.isVerified()) {
|
||||
verifyDevice(userId, device);
|
||||
}
|
||||
};
|
||||
|
||||
const deviceName = device.ambiguous ?
|
||||
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted");
|
||||
return (<AccessibleButton className={classes} onClick={onDeviceClick}>
|
||||
<div className={iconClasses} />
|
||||
<div className="mx_UserInfo_device_name">{deviceName}</div>
|
||||
<div className="mx_UserInfo_device_trusted">{trustedLabel}</div>
|
||||
</AccessibleButton>);
|
||||
}
|
||||
|
||||
function DevicesSection({devices, userId, loading}) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const [isExpanded, setExpanded] = useState(false);
|
||||
|
||||
if (loading) {
|
||||
// still loading
|
||||
return <Spinner />;
|
||||
|
@ -74,123 +156,50 @@ const DevicesSection = ({devices, userId, loading}) => {
|
|||
if (devices === null) {
|
||||
return _t("Unable to load device list");
|
||||
}
|
||||
if (devices.length === 0) {
|
||||
return _t("No devices with registered encryption keys");
|
||||
|
||||
const unverifiedDevices = devices.filter(d => !d.isVerified());
|
||||
const verifiedDevices = devices.filter(d => d.isVerified());
|
||||
|
||||
let expandButton;
|
||||
if (verifiedDevices.length) {
|
||||
if (isExpanded) {
|
||||
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(false)}>
|
||||
<div>{_t("Hide verified Sign-In's")}</div>
|
||||
</AccessibleButton>);
|
||||
} else {
|
||||
expandButton = (<AccessibleButton className="mx_UserInfo_expand" onClick={() => setExpanded(true)}>
|
||||
<div className="mx_E2EIcon mx_E2EIcon_verified" />
|
||||
<div>{_t("%(count)s verified Sign-In's", {count: verifiedDevices.length})}</div>
|
||||
</AccessibleButton>);
|
||||
}
|
||||
}
|
||||
|
||||
let deviceList = unverifiedDevices.map((device, i) => {
|
||||
return (<DeviceItem key={i} userId={userId} device={device} />);
|
||||
});
|
||||
if (isExpanded) {
|
||||
const keyStart = unverifiedDevices.length;
|
||||
deviceList = deviceList.concat(verifiedDevices.map((device, i) => {
|
||||
return (<DeviceItem key={i + keyStart} userId={userId} device={device} />);
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Trust & Devices") }</h3>
|
||||
<div className="mx_UserInfo_devices">
|
||||
{ devices.map((device, i) => <MemberDeviceInfo key={i} userId={userId} device={device} />) }
|
||||
<div>{deviceList}</div>
|
||||
<div>{expandButton}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const onRoomTileClick = (roomId) => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
});
|
||||
};
|
||||
|
||||
const DirectChatsSection = withLegacyMatrixClient(({matrixClient: cli, userId, startUpdating, stopUpdating}) => {
|
||||
const onNewDMClick = async () => {
|
||||
startUpdating();
|
||||
await createRoom({dmUserId: userId});
|
||||
stopUpdating();
|
||||
};
|
||||
|
||||
// TODO: Immutable DMs replaces a lot of this
|
||||
// dmRooms will not include dmRooms that we have been invited into but did not join.
|
||||
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
|
||||
// XXX: we potentially want DMs we have been invited to, to also show up here :L
|
||||
// especially as logic below concerns specially if we haven't joined but have been invited
|
||||
const [dmRooms, setDmRooms] = useState(new DMRoomMap(cli).getDMRoomsForUserId(userId));
|
||||
|
||||
// TODO bind the below
|
||||
// cli.on("Room", this.onRoom);
|
||||
// cli.on("Room.name", this.onRoomName);
|
||||
// cli.on("deleteRoom", this.onDeleteRoom);
|
||||
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.direct") {
|
||||
const dmRoomMap = new DMRoomMap(cli);
|
||||
setDmRooms(dmRoomMap.getDMRoomsForUserId(userId));
|
||||
}
|
||||
}, [cli, userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
const RoomTile = sdk.getComponent("rooms.RoomTile");
|
||||
|
||||
const tiles = [];
|
||||
for (const roomId of dmRooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room) {
|
||||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
if (myMembership !== 'join') continue;
|
||||
|
||||
const them = room.getMember(userId);
|
||||
// not a DM room if they are not joined
|
||||
if (!them || !them.membership || them.membership !== 'join') continue;
|
||||
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId}
|
||||
room={room}
|
||||
transparent={true}
|
||||
collapsed={false}
|
||||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={false}
|
||||
onClick={onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const labelClasses = classNames({
|
||||
mx_UserInfo_createRoom_label: true,
|
||||
mx_RoomTile_name: true,
|
||||
});
|
||||
|
||||
let body = tiles;
|
||||
if (!body) {
|
||||
body = (
|
||||
<AccessibleButton className="mx_UserInfo_createRoom" onClick={onNewDMClick}>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<img src={require("../../../../res/img/create-big.svg")} width="26" height="26" alt={_t("Start a chat")} />
|
||||
</div>
|
||||
<div className={labelClasses}><i>{ _t("Start a chat") }</i></div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_container_header">
|
||||
<h3>{ _t("Direct messages") }</h3>
|
||||
<AccessibleButton
|
||||
className="mx_UserInfo_container_header_right mx_UserInfo_newDmButton"
|
||||
onClick={onNewDMClick}
|
||||
title={_t("Start a chat")}
|
||||
/>
|
||||
</div>
|
||||
{ body }
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite}) => {
|
||||
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => {
|
||||
let ignoreButton = null;
|
||||
let insertPillButton = null;
|
||||
let inviteUserButton = null;
|
||||
let readReceiptButton = null;
|
||||
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
const onShareUserClick = () => {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
|
||||
|
@ -200,7 +209,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
|
||||
// Only allow the user to ignore the user if its not ourselves
|
||||
// same goes for jumping to read receipt
|
||||
if (member.userId !== cli.getUserId()) {
|
||||
if (!isMe) {
|
||||
const onIgnoreToggle = () => {
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
if (isIgnored) {
|
||||
|
@ -214,7 +223,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
};
|
||||
|
||||
ignoreButton = (
|
||||
<AccessibleButton onClick={onIgnoreToggle} className="mx_UserInfo_field">
|
||||
<AccessibleButton onClick={onIgnoreToggle} className={classNames("mx_UserInfo_field", {mx_UserInfo_destructive: !isIgnored})}>
|
||||
{ isIgnored ? _t("Unignore") : _t("Ignore") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -285,15 +294,34 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
|||
</AccessibleButton>
|
||||
);
|
||||
|
||||
let directMessageButton;
|
||||
if (!isMe) {
|
||||
directMessageButton = (
|
||||
<AccessibleButton onClick={() => openDMForUser(cli, member.userId)} className="mx_UserInfo_field">
|
||||
{ _t('Direct message') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
let unverifyButton;
|
||||
if (devices && devices.some(device => device.isVerified())) {
|
||||
unverifyButton = (
|
||||
<AccessibleButton onClick={() => unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{ _t('Unverify user') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("User Options") }</h3>
|
||||
<div className="mx_UserInfo_buttons">
|
||||
<h3>{ _t("Options") }</h3>
|
||||
<div>
|
||||
{ directMessageButton }
|
||||
{ readReceiptButton }
|
||||
{ shareUserButton }
|
||||
{ insertPillButton }
|
||||
{ ignoreButton }
|
||||
{ inviteUserButton }
|
||||
{ ignoreButton }
|
||||
{ unverifyButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -337,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => {
|
|||
return member.powerLevel < levelToSend;
|
||||
};
|
||||
|
||||
const useRoomPowerLevels = (room) => {
|
||||
const useRoomPowerLevels = (cli, room) => {
|
||||
const [powerLevels, setPowerLevels] = useState({});
|
||||
|
||||
const update = useCallback(() => {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
const event = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (event) {
|
||||
setPowerLevels(event.getContent());
|
||||
|
@ -352,7 +383,7 @@ const useRoomPowerLevels = (room) => {
|
|||
};
|
||||
}, [room]);
|
||||
|
||||
useEventEmitter(room, "RoomState.events", update);
|
||||
useEventEmitter(cli, "RoomState.members", update);
|
||||
useEffect(() => {
|
||||
update();
|
||||
return () => {
|
||||
|
@ -399,7 +430,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
|
|||
};
|
||||
|
||||
const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick");
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onKick}>
|
||||
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
|
||||
{ kickLabel }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
@ -472,7 +503,7 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}
|
|||
}
|
||||
};
|
||||
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onRedactAllMessages}>
|
||||
return <AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onRedactAllMessages}>
|
||||
{ _t("Remove recent messages") }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
@ -524,7 +555,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
|
|||
label = _t("Unban");
|
||||
}
|
||||
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onBanOrUnban}>
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: member.membership !== 'ban',
|
||||
});
|
||||
|
||||
return <AccessibleButton className={classes} onClick={onBanOrUnban}>
|
||||
{ label }
|
||||
</AccessibleButton>;
|
||||
});
|
||||
|
@ -581,21 +616,24 @@ const MuteToggleButton = withLegacyMatrixClient(
|
|||
}
|
||||
};
|
||||
|
||||
const classes = classNames("mx_UserInfo_field", {
|
||||
mx_UserInfo_destructive: !isMuted,
|
||||
});
|
||||
|
||||
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
|
||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onMuteToggle}>
|
||||
return <AccessibleButton className={classes} onClick={onMuteToggle}>
|
||||
{ muteLabel }
|
||||
</AccessibleButton>;
|
||||
},
|
||||
);
|
||||
|
||||
const RoomAdminToolsContainer = withLegacyMatrixClient(
|
||||
({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => {
|
||||
({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => {
|
||||
let kickButton;
|
||||
let banButton;
|
||||
let muteButton;
|
||||
let redactButton;
|
||||
|
||||
const powerLevels = useRoomPowerLevels(room);
|
||||
const editPowerLevel = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
|
@ -705,7 +743,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient(
|
|||
};
|
||||
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={_onKick}>
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={_onKick}>
|
||||
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -744,47 +782,17 @@ const useIsSynapseAdmin = (cli) => {
|
|||
return isAdmin;
|
||||
};
|
||||
|
||||
// cli is injected by withLegacyMatrixClient
|
||||
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
|
||||
// Load room if we are given a room id and memoize it
|
||||
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}, [cli, user.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}
|
||||
}, [cli, user.userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
function useRoomPermissions(cli, room, user) {
|
||||
const [roomPermissions, setRoomPermissions] = useState({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
modifyLevelMax: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
const updateRoomPermissions = useCallback(async () => {
|
||||
if (!room) return;
|
||||
const updateRoomPermissions = useCallback(() => {
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
@ -811,20 +819,197 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
|
||||
setRoomPermissions({
|
||||
canInvite: me.powerLevel >= powerLevels.invite,
|
||||
canEdit: modifyLevelMax >= 0,
|
||||
modifyLevelMax,
|
||||
});
|
||||
}, [cli, user, room]);
|
||||
useEventEmitter(cli, "RoomState.events", updateRoomPermissions);
|
||||
useEventEmitter(cli, "RoomState.members", updateRoomPermissions);
|
||||
useEffect(() => {
|
||||
updateRoomPermissions();
|
||||
return () => {
|
||||
setRoomPermissions({
|
||||
maximalPowerLevel: -1,
|
||||
canEdit: false,
|
||||
canInvite: false,
|
||||
});
|
||||
};
|
||||
}, [updateRoomPermissions]);
|
||||
|
||||
return roomPermissions;
|
||||
}
|
||||
|
||||
const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => {
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
if (room && user.roomId) { // is in room
|
||||
if (isEditing) {
|
||||
return (<PowerLevelEditor
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
onFinished={() => setEditing(false)} />);
|
||||
} else {
|
||||
const IconButton = sdk.getComponent('elements.IconButton');
|
||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||
const powerLevel = parseInt(user.powerLevel, 10);
|
||||
const modifyButton = roomPermissions.canEdit ?
|
||||
(<IconButton icon="edit" onClick={() => setEditing(true)} />) : null;
|
||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||
const label = _t("<strong>%(role)s</strong> in %(roomName)s",
|
||||
{role, roomName: room.name},
|
||||
{strong: label => <strong>{label}</strong>},
|
||||
);
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div className="mx_UserInfo_roleDescription">{label}{modifyButton}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const onPowerChange = useCallback((powerLevel) => {
|
||||
setIsDirty(true);
|
||||
setSelectedPowerLevel(parseInt(powerLevel, 10));
|
||||
}, [setSelectedPowerLevel, setIsDirty]);
|
||||
|
||||
const changePowerLevel = useCallback(async () => {
|
||||
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to change power level"),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
if (!isDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdating(true);
|
||||
|
||||
const powerLevel = selectedPowerLevel;
|
||||
|
||||
const roomId = user.roomId;
|
||||
const target = user.userId;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
if (!powerLevelEvent.getContent().users) {
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
const myUserId = cli.getUserId();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
try {
|
||||
if (!(await _warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
const myPower = powerLevelEvent.getContent().users[myUserId];
|
||||
if (parseInt(myPower) === parseInt(powerLevel)) {
|
||||
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
});
|
||||
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) return;
|
||||
}
|
||||
await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} finally {
|
||||
onFinished();
|
||||
}
|
||||
}, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]);
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||
const IconButton = sdk.getComponent('elements.IconButton');
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
const buttonOrSpinner = isUpdating ? <Spinner w={16} h={16} /> :
|
||||
<IconButton icon="check" onClick={changePowerLevel} />;
|
||||
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
return (
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
label={null}
|
||||
value={selectedPowerLevel}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={onPowerChange}
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
{buttonOrSpinner}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// cli is injected by withLegacyMatrixClient
|
||||
const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => {
|
||||
// Load room if we are given a room id and memoize it
|
||||
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
|
||||
|
||||
// only display the devices list if our client supports E2E
|
||||
const _enableDevices = cli.isCryptoEnabled();
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
// Load whether or not we are a Synapse Admin
|
||||
const isSynapseAdmin = useIsSynapseAdmin(cli);
|
||||
|
||||
// Check whether the user is ignored
|
||||
const [isIgnored, setIsIgnored] = useState(cli.isUserIgnored(user.userId));
|
||||
// Recheck if the user or client changes
|
||||
useEffect(() => {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}, [cli, user.userId]);
|
||||
// Recheck also if we receive new accountData m.ignored_user_list
|
||||
const accountDataHandler = useCallback((ev) => {
|
||||
if (ev.getType() === "m.ignored_user_list") {
|
||||
setIsIgnored(cli.isUserIgnored(user.userId));
|
||||
}
|
||||
}, [cli, user.userId]);
|
||||
useEventEmitter(cli, "accountData", accountDataHandler);
|
||||
|
||||
// Count of how many operations are currently in progress, if > 0 then show a Spinner
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const startUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount + 1);
|
||||
}, [pendingUpdateCount]);
|
||||
const stopUpdating = useCallback(() => {
|
||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||
}, [pendingUpdateCount]);
|
||||
|
||||
const roomPermissions = useRoomPermissions(cli, room, user);
|
||||
|
||||
const onSynapseDeactivate = useCallback(async () => {
|
||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||
|
@ -852,71 +1037,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
}
|
||||
}, [cli, user.userId]);
|
||||
|
||||
const onPowerChange = useCallback(async (powerLevel) => {
|
||||
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
||||
startUpdating();
|
||||
cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
||||
function() {
|
||||
// NO-OP; rely on the m.room.member event coming down else we could
|
||||
// get out of sync if we force setState here!
|
||||
console.log("Power change success");
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to change power level " + err);
|
||||
Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Failed to change power level"),
|
||||
});
|
||||
},
|
||||
).finally(() => {
|
||||
stopUpdating();
|
||||
}).done();
|
||||
|
||||
const onMemberAvatarKey = e => {
|
||||
if (e.key === "Enter") {
|
||||
onMemberAvatarClick();
|
||||
}
|
||||
};
|
||||
|
||||
const roomId = user.roomId;
|
||||
const target = user.userId;
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
if (!powerLevelEvent.getContent().users) {
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
const myUserId = cli.getUserId();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
try {
|
||||
if (!(await _warnSelfDemote())) return;
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const myPower = powerLevelEvent.getContent().users[myUserId];
|
||||
if (parseInt(myPower) === parseInt(powerLevel)) {
|
||||
const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
});
|
||||
|
||||
const [confirmed] = await finished;
|
||||
if (confirmed) return;
|
||||
}
|
||||
_applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}, [user.roomId, user.userId, room && room.currentState, cli]); // eslint-disable-line
|
||||
|
||||
const onMemberAvatarClick = useCallback(() => {
|
||||
const member = user;
|
||||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
|
@ -935,17 +1062,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
let synapseDeactivateButton;
|
||||
let spinner;
|
||||
|
||||
let directChatsSection;
|
||||
if (user.userId !== cli.getUserId()) {
|
||||
directChatsSection = <DirectChatsSection userId={user.userId} />;
|
||||
}
|
||||
|
||||
// We don't need a perfect check here, just something to pass as "probably not our homeserver". If
|
||||
// someone does figure out how to bypass this check the worst that happens is an error.
|
||||
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
|
||||
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
|
||||
synapseDeactivateButton = (
|
||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field">
|
||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||
{_t("Deactivate user")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -955,6 +1077,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
if (room && user.roomId) {
|
||||
adminToolsContainer = (
|
||||
<RoomAdminToolsContainer
|
||||
powerLevels={powerLevels}
|
||||
member={user}
|
||||
room={room}
|
||||
startUpdating={startUpdating}
|
||||
|
@ -992,7 +1115,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
let presenceCurrentlyActive;
|
||||
let statusMessage;
|
||||
|
||||
if (user instanceof RoomMember) {
|
||||
if (user instanceof RoomMember && user.user) {
|
||||
presenceState = user.user.presence;
|
||||
presenceLastActiveAgo = user.user.lastActiveAgo;
|
||||
presenceCurrentlyActive = user.user.currentlyActive;
|
||||
|
@ -1021,32 +1144,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
||||
}
|
||||
|
||||
let memberDetails = null;
|
||||
|
||||
if (room && user.roomId) { // is in room
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0;
|
||||
|
||||
const PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
memberDetails = <div>
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<PowerSelector
|
||||
value={parseInt(user.powerLevel)}
|
||||
maxValue={roomPermissions.modifyLevelMax}
|
||||
disabled={roomPermissions.modifyLevelMax < 0}
|
||||
usersDefault={powerLevelUsersDefault}
|
||||
onChange={onPowerChange} />
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||
avatarElement = <div className="mx_UserInfo_avatar" onClick={onMemberAvatarClick}>
|
||||
<img src={httpUrl} alt={_t("Profile picture")} />
|
||||
avatarElement = <div
|
||||
className="mx_UserInfo_avatar"
|
||||
onClick={onMemberAvatarClick}
|
||||
onKeyDown={onMemberAvatarKey}
|
||||
tabIndex="0"
|
||||
role="img"
|
||||
aria-label={_t("Profile picture")}
|
||||
>
|
||||
<div><div style={{backgroundImage: `url(${httpUrl})`}} /></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
@ -1058,6 +1168,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
title={_t('Close')} />;
|
||||
}
|
||||
|
||||
const memberDetails = <PowerLevelSection
|
||||
powerLevels={powerLevels}
|
||||
user={user} room={room} roomPermissions={roomPermissions}
|
||||
/>;
|
||||
|
||||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||
const [devices, setDevices] = useState(undefined);
|
||||
// Download device lists
|
||||
|
@ -1082,14 +1198,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
setDevices(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (isRoomEncrypted) {
|
||||
_downloadDeviceList();
|
||||
}
|
||||
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [cli, user.userId]);
|
||||
}, [cli, user.userId, isRoomEncrypted]);
|
||||
|
||||
// Listen to changes
|
||||
useEffect(() => {
|
||||
|
@ -1106,21 +1223,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
}
|
||||
};
|
||||
|
||||
if (isRoomEncrypted) {
|
||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
}
|
||||
// Handle being unmounted
|
||||
return () => {
|
||||
cancel = true;
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
};
|
||||
}, [cli, user.userId]);
|
||||
|
||||
let devicesSection;
|
||||
const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId);
|
||||
if (isRoomEncrypted) {
|
||||
devicesSection = <DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />;
|
||||
} else {
|
||||
let text;
|
||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||
}
|
||||
};
|
||||
}, [cli, user.userId, isRoomEncrypted]);
|
||||
|
||||
let text;
|
||||
if (!isRoomEncrypted) {
|
||||
if (!_enableDevices) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room) {
|
||||
|
@ -1128,22 +1244,24 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
} else {
|
||||
// TODO what to render for GroupMember
|
||||
}
|
||||
} else {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
if (text) {
|
||||
devicesSection = (
|
||||
const devicesSection = isRoomEncrypted ?
|
||||
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
|
||||
const securitySection = (
|
||||
<div className="mx_UserInfo_container">
|
||||
<h3>{ _t("Trust & Devices") }</h3>
|
||||
<div className="mx_UserInfo_devices">
|
||||
{ text }
|
||||
</div>
|
||||
<h3>{ _t("Security") }</h3>
|
||||
<p>{ text }</p>
|
||||
<AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>{_t("Verify")}</AccessibleButton>
|
||||
{ devicesSection }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let e2eIcon;
|
||||
if (isRoomEncrypted && devices) {
|
||||
e2eIcon = <E2EIcon status={_getE2EStatus(devices)} isUser={true} />;
|
||||
e2eIcon = <E2EIcon size={18} status={_getE2EStatus(devices)} isUser={true} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1153,16 +1271,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
|
||||
<div className="mx_UserInfo_container">
|
||||
<div className="mx_UserInfo_profile">
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<h2>
|
||||
<div >
|
||||
<h2 aria-label={displayName}>
|
||||
{ e2eIcon }
|
||||
{ displayName }
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx_UserInfo_profileField">
|
||||
{ user.userId }
|
||||
</div>
|
||||
<div className="mx_UserInfo_profileField">
|
||||
<div>{ user.userId }</div>
|
||||
<div className="mx_UserInfo_profileStatus">
|
||||
{presenceLabel}
|
||||
{statusLabel}
|
||||
</div>
|
||||
|
@ -1176,11 +1292,9 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
|||
</div> }
|
||||
|
||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||
{ devicesSection }
|
||||
|
||||
{ directChatsSection }
|
||||
|
||||
{ securitySection }
|
||||
<UserOptionsSection
|
||||
devices={devices}
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={user} />
|
||||
|
|
|
@ -36,7 +36,13 @@ export default function(props) {
|
|||
_t("All devices for this user are trusted") :
|
||||
_t("All devices in this encrypted room are trusted");
|
||||
}
|
||||
const icon = (<div className={e2eIconClasses} title={e2eTitle} />);
|
||||
|
||||
let style = null;
|
||||
if (props.size) {
|
||||
style = {width: `${props.size}px`, height: `${props.size}px`};
|
||||
}
|
||||
|
||||
const icon = (<div className={e2eIconClasses} style={style} title={e2eTitle} />);
|
||||
if (props.onClick) {
|
||||
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
||||
} else {
|
||||
|
|
|
@ -606,8 +606,8 @@ module.exports = createReactClass({
|
|||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: this.state.verified === true,
|
||||
mx_EventTile_unverified: this.state.verified === false,
|
||||
mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
|
||||
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_redacted: isRedacted,
|
||||
|
@ -800,7 +800,7 @@ module.exports = createReactClass({
|
|||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
@ -826,7 +826,7 @@ module.exports = createReactClass({
|
|||
{ readAvatars }
|
||||
</div>
|
||||
{ sender }
|
||||
<div className={classNames("mx_EventTile_line", {mx_EventTile_bubbleLine: isBubbleMessage})}>
|
||||
<div className="mx_EventTile_line">
|
||||
<a
|
||||
href={permalink}
|
||||
onClick={this.onPermalinkClicked}
|
||||
|
@ -834,7 +834,7 @@ module.exports = createReactClass({
|
|||
>
|
||||
{ timestamp }
|
||||
</a>
|
||||
{ this._renderE2EPadlock() }
|
||||
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||
{ thread }
|
||||
<EventTileType ref="tile"
|
||||
mxEvent={this.props.mxEvent}
|
||||
|
|
|
@ -118,6 +118,7 @@
|
|||
"Restricted": "Restricted",
|
||||
"Moderator": "Moderator",
|
||||
"Admin": "Admin",
|
||||
"Custom (%(level)s)": "Custom (%(level)s)",
|
||||
"Start a chat": "Start a chat",
|
||||
"Who would you like to communicate with?": "Who would you like to communicate with?",
|
||||
"Email, name or Matrix ID": "Email, name or Matrix ID",
|
||||
|
@ -1066,16 +1067,26 @@
|
|||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
|
||||
"Members": "Members",
|
||||
"Files": "Files",
|
||||
"Trust & Devices": "Trust & Devices",
|
||||
"Direct messages": "Direct messages",
|
||||
"Trusted": "Trusted",
|
||||
"Not trusted": "Not trusted",
|
||||
"Hide verified Sign-In's": "Hide verified Sign-In's",
|
||||
"%(count)s verified Sign-In's|other": "%(count)s verified Sign-In's",
|
||||
"%(count)s verified Sign-In's|one": "1 verified Sign-In",
|
||||
"Direct message": "Direct message",
|
||||
"Unverify user": "Unverify user",
|
||||
"Options": "Options",
|
||||
"Remove from community": "Remove from community",
|
||||
"Disinvite this user from community?": "Disinvite this user from community?",
|
||||
"Remove this user from community?": "Remove this user from community?",
|
||||
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
||||
"Failed to remove user from community": "Failed to remove user from community",
|
||||
"<strong>%(role)s</strong> in %(roomName)s": "<strong>%(role)s</strong> in %(roomName)s",
|
||||
"Failed to deactivate user": "Failed to deactivate user",
|
||||
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
|
||||
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
|
||||
"Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.",
|
||||
"Security": "Security",
|
||||
"Verify": "Verify",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
"Tuesday": "Tuesday",
|
||||
|
@ -1091,7 +1102,6 @@
|
|||
"Reply": "Reply",
|
||||
"Edit": "Edit",
|
||||
"Message Actions": "Message Actions",
|
||||
"Options": "Options",
|
||||
"Attachment": "Attachment",
|
||||
"Error decrypting attachment": "Error decrypting attachment",
|
||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||
|
|
Loading…
Reference in a new issue