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/_ErrorBoundary.scss";
|
||||||
@import "./views/elements/_EventListSummary.scss";
|
@import "./views/elements/_EventListSummary.scss";
|
||||||
@import "./views/elements/_Field.scss";
|
@import "./views/elements/_Field.scss";
|
||||||
|
@import "./views/elements/_IconButton.scss";
|
||||||
@import "./views/elements/_ImageView.scss";
|
@import "./views/elements/_ImageView.scss";
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
@import "./views/elements/_InteractiveTooltip.scss";
|
@import "./views/elements/_InteractiveTooltip.scss";
|
||||||
|
|
|
@ -49,6 +49,7 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field select {
|
.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;
|
width: 12px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
content: "";
|
content: "";
|
||||||
mask: url("$(res)/img/e2e/verified.svg");
|
mask: url("$(res)/img/e2e/normal.svg");
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: 100%;
|
mask-size: 100%;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
@ -33,6 +33,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_KeyVerification_icon_verified::after {
|
&.mx_KeyVerification_icon_verified::after {
|
||||||
|
mask: url("$(res)/img/e2e/verified.svg");
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,16 +20,9 @@ limitations under the License.
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
font-size: 12px;
|
||||||
|
|
||||||
.mx_UserInfo_profile .mx_E2EIcon {
|
.mx_UserInfo_cancel {
|
||||||
display: inline;
|
|
||||||
margin: auto;
|
|
||||||
padding-right: 25px;
|
|
||||||
mask-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_cancel {
|
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
padding: 10px 0 10px 10px;
|
padding: 10px 0 10px 10px;
|
||||||
|
@ -38,138 +31,211 @@ limitations under the License.
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: 16px center;
|
mask-position: 16px center;
|
||||||
background-color: $rightpanel-button-color;
|
background-color: $rightpanel-button-color;
|
||||||
}
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserInfo_profile h2 {
|
h2 {
|
||||||
flex: 1;
|
font-size: 18px;
|
||||||
overflow-x: auto;
|
|
||||||
max-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo h2 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 16px 0 8px 0;
|
margin: 18px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_container {
|
.mx_UserInfo_container {
|
||||||
padding: 0 16px 16px 16px;
|
padding: 0 16px 16px 16px;
|
||||||
border-bottom: 1px solid lightgray;
|
border-bottom: 1px solid lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_memberDetailsContainer {
|
.mx_UserInfo_memberDetailsContainer {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo .mx_RoomTile_nameContainer {
|
.mx_RoomTile_nameContainer {
|
||||||
width: 154px;
|
width: 154px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo .mx_RoomTile_badge {
|
.mx_RoomTile_badge {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo .mx_RoomTile_name {
|
.mx_RoomTile_name {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_avatar {
|
.mx_UserInfo_avatar {
|
||||||
background: $tagpanel-bg-color;
|
margin: 24px 32px 0 32px;
|
||||||
}
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserInfo_avatar > img {
|
.mx_UserInfo_avatar > div {
|
||||||
height: auto;
|
max-width: 30vh;
|
||||||
width: 100%;
|
margin: 0 auto;
|
||||||
max-height: 30vh;
|
}
|
||||||
object-fit: contain;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_avatar .mx_BaseAvatar.mx_BaseAvatar_image {
|
.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;
|
cursor: zoom-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo h3 {
|
h3 {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: $input-darker-fg-color;
|
color: $notice-secondary-color;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_UserInfo_profileField {
|
p {
|
||||||
font-size: 15px;
|
margin: 5px 0;
|
||||||
position: relative;
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo_profile {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_memberDetails {
|
h2 {
|
||||||
text-align: center;
|
font-size: 18px;
|
||||||
}
|
line-height: 25px;
|
||||||
|
flex: 1;
|
||||||
.mx_UserInfo_field {
|
overflow-x: auto;
|
||||||
cursor: pointer;
|
max-height: 50px;
|
||||||
font-size: 15px;
|
|
||||||
color: $primary-fg-color;
|
|
||||||
margin-left: 8px;
|
|
||||||
line-height: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_createRoom {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_createRoom_label {
|
.mx_E2EIcon {
|
||||||
width: initial !important;
|
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;
|
cursor: pointer;
|
||||||
}
|
color: $accent-color;
|
||||||
|
line-height: 16px;
|
||||||
|
margin: 8px 0;
|
||||||
|
|
||||||
.mx_UserInfo_statusMessage {
|
&.mx_UserInfo_destructive {
|
||||||
|
color: $warning-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo_statusMessage {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
}
|
}
|
||||||
.mx_UserInfo .mx_UserInfo_scrollContainer {
|
|
||||||
flex: 1;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo .mx_UserInfo_scrollContainer .mx_UserInfo_container {
|
.mx_UserInfo_scrollContainer {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo_scrollContainer .mx_UserInfo_container {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_container_header {
|
> :not(h3) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_UserInfo_devices {
|
||||||
|
.mx_UserInfo_device {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_container_header_right {
|
&.mx_UserInfo_device_verified {
|
||||||
position: relative;
|
.mx_UserInfo_device_trusted {
|
||||||
margin-left: auto;
|
color: $accent-color;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
&.mx_UserInfo_device_unverified {
|
||||||
|
.mx_UserInfo_device_trusted {
|
||||||
|
color: $warning-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_UserInfo_newDmButton {
|
.mx_UserInfo_device_name {
|
||||||
background-color: $roomheader-addroom-bg-color;
|
flex: 1;
|
||||||
border-radius: 10px; // 16/2 + 2 padding
|
margin-right: 5px;
|
||||||
height: 16px;
|
}
|
||||||
flex: 0 0 16px;
|
}
|
||||||
|
|
||||||
&::before {
|
// both for icon in expand button and device item
|
||||||
background-color: $roomheader-addroom-fg-color;
|
.mx_E2EIcon {
|
||||||
mask: url('$(res)/img/icons-room-add.svg');
|
// don't squeeze
|
||||||
mask-repeat: no-repeat;
|
flex: 0 0 auto;
|
||||||
mask-position: center;
|
margin: 2px 5px 0 0;
|
||||||
content: '';
|
width: 12px;
|
||||||
position: absolute;
|
height: 12px;
|
||||||
top: 0;
|
}
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
.mx_UserInfo_expand {
|
||||||
right: 0;
|
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 {
|
.mx_E2EIcon {
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center 0;
|
|
||||||
margin: 0 9px;
|
margin: 0 9px;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_E2EIcon_verified {
|
.mx_E2EIcon_verified::before, .mx_E2EIcon_warning::before {
|
||||||
mask-image: url('$(res)/img/e2e/lock-verified.svg');
|
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;
|
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;
|
background-color: $warning-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,10 @@ limitations under the License.
|
||||||
.mx_MessageComposer_e2eIcon.mx_E2EIcon {
|
.mx_MessageComposer_e2eIcon.mx_E2EIcon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 60px;
|
left: 60px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
background-color: $composer-e2e-icon-color;
|
background-color: $composer-e2e-icon-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_noperm_error {
|
.mx_MessageComposer_noperm_error {
|
||||||
|
|
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">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<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"/>
|
<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>
|
</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">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<defs>
|
<svg
|
||||||
<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"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</defs>
|
width="24"
|
||||||
<use fill="#F56679" fill-rule="evenodd" stroke="#F56679" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" transform="translate(1 1)" xlink:href="#a"/>
|
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>
|
</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) {
|
export function textualPowerLevel(level, usersDefault) {
|
||||||
const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
|
const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
|
||||||
if (LEVEL_ROLE_MAP[level]) {
|
if (LEVEL_ROLE_MAP[level]) {
|
||||||
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
|
return LEVEL_ROLE_MAP[level];
|
||||||
} else {
|
} 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() {
|
render: function() {
|
||||||
let picker;
|
let picker;
|
||||||
|
const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label;
|
||||||
if (this.state.custom) {
|
if (this.state.custom) {
|
||||||
picker = (
|
picker = (
|
||||||
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
|
<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}
|
onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} onChange={this.onCustomChange}
|
||||||
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
value={String(this.state.customValue)} disabled={this.props.disabled} />
|
||||||
);
|
);
|
||||||
|
@ -151,7 +152,7 @@ module.exports = createReactClass({
|
||||||
|
|
||||||
picker = (
|
picker = (
|
||||||
<Field id={`powerSelector_notCustom_${this.props.powerLevelKey}`} element="select"
|
<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}>
|
value={String(this.state.selectValue)} disabled={this.props.disabled}>
|
||||||
{options}
|
{options}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -27,7 +27,6 @@ import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import createRoom from '../../../createRoom';
|
import createRoom from '../../../createRoom';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import Unread from '../../../Unread';
|
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -40,6 +39,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon from "../rooms/E2EIcon";
|
||||||
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
|
import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
|
import {textualPowerLevel} from '../../../Roles';
|
||||||
|
|
||||||
const _disambiguateDevices = (devices) => {
|
const _disambiguateDevices = (devices) => {
|
||||||
const names = Object.create(null);
|
const names = Object.create(null);
|
||||||
|
@ -63,10 +63,92 @@ const _getE2EStatus = (devices) => {
|
||||||
return hasUnverifiedDevice ? "warning" : "verified";
|
return hasUnverifiedDevice ? "warning" : "verified";
|
||||||
};
|
};
|
||||||
|
|
||||||
const DevicesSection = ({devices, userId, loading}) => {
|
async function unverifyUser(matrixClient, userId) {
|
||||||
const MemberDeviceInfo = sdk.getComponent('rooms.MemberDeviceInfo');
|
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 Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
|
||||||
|
const [isExpanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
// still loading
|
// still loading
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
|
@ -74,123 +156,50 @@ const DevicesSection = ({devices, userId, loading}) => {
|
||||||
if (devices === null) {
|
if (devices === null) {
|
||||||
return _t("Unable to load device list");
|
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 (
|
return (
|
||||||
<div className="mx_UserInfo_container">
|
|
||||||
<h3>{ _t("Trust & Devices") }</h3>
|
|
||||||
<div className="mx_UserInfo_devices">
|
<div className="mx_UserInfo_devices">
|
||||||
{ devices.map((device, i) => <MemberDeviceInfo key={i} userId={userId} device={device} />) }
|
<div>{deviceList}</div>
|
||||||
</div>
|
<div>{expandButton}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const onRoomTileClick = (roomId) => {
|
const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => {
|
||||||
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}) => {
|
|
||||||
let ignoreButton = null;
|
let ignoreButton = null;
|
||||||
let insertPillButton = null;
|
let insertPillButton = null;
|
||||||
let inviteUserButton = null;
|
let inviteUserButton = null;
|
||||||
let readReceiptButton = null;
|
let readReceiptButton = null;
|
||||||
|
|
||||||
|
const isMe = member.userId === cli.getUserId();
|
||||||
|
|
||||||
const onShareUserClick = () => {
|
const onShareUserClick = () => {
|
||||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||||
Modal.createTrackedDialog('share room member dialog', '', 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
|
// Only allow the user to ignore the user if its not ourselves
|
||||||
// same goes for jumping to read receipt
|
// same goes for jumping to read receipt
|
||||||
if (member.userId !== cli.getUserId()) {
|
if (!isMe) {
|
||||||
const onIgnoreToggle = () => {
|
const onIgnoreToggle = () => {
|
||||||
const ignoredUsers = cli.getIgnoredUsers();
|
const ignoredUsers = cli.getIgnoredUsers();
|
||||||
if (isIgnored) {
|
if (isIgnored) {
|
||||||
|
@ -214,7 +223,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
||||||
};
|
};
|
||||||
|
|
||||||
ignoreButton = (
|
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") }
|
{ isIgnored ? _t("Unignore") : _t("Ignore") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -285,15 +294,34 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
|
||||||
</AccessibleButton>
|
</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 (
|
return (
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
<h3>{ _t("User Options") }</h3>
|
<h3>{ _t("Options") }</h3>
|
||||||
<div className="mx_UserInfo_buttons">
|
<div>
|
||||||
|
{ directMessageButton }
|
||||||
{ readReceiptButton }
|
{ readReceiptButton }
|
||||||
{ shareUserButton }
|
{ shareUserButton }
|
||||||
{ insertPillButton }
|
{ insertPillButton }
|
||||||
{ ignoreButton }
|
|
||||||
{ inviteUserButton }
|
{ inviteUserButton }
|
||||||
|
{ ignoreButton }
|
||||||
|
{ unverifyButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -337,10 +365,13 @@ const _isMuted = (member, powerLevelContent) => {
|
||||||
return member.powerLevel < levelToSend;
|
return member.powerLevel < levelToSend;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useRoomPowerLevels = (room) => {
|
const useRoomPowerLevels = (cli, room) => {
|
||||||
const [powerLevels, setPowerLevels] = useState({});
|
const [powerLevels, setPowerLevels] = useState({});
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
|
if (!room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const event = room.currentState.getStateEvents("m.room.power_levels", "");
|
const event = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||||
if (event) {
|
if (event) {
|
||||||
setPowerLevels(event.getContent());
|
setPowerLevels(event.getContent());
|
||||||
|
@ -352,7 +383,7 @@ const useRoomPowerLevels = (room) => {
|
||||||
};
|
};
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEventEmitter(room, "RoomState.events", update);
|
useEventEmitter(cli, "RoomState.members", update);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
update();
|
update();
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -399,7 +430,7 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start
|
||||||
};
|
};
|
||||||
|
|
||||||
const kickLabel = member.membership === "invite" ? _t("Disinvite") : _t("Kick");
|
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 }
|
{ kickLabel }
|
||||||
</AccessibleButton>;
|
</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") }
|
{ _t("Remove recent messages") }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
});
|
});
|
||||||
|
@ -524,7 +555,11 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star
|
||||||
label = _t("Unban");
|
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 }
|
{ label }
|
||||||
</AccessibleButton>;
|
</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");
|
const muteLabel = isMuted ? _t("Unmute") : _t("Mute");
|
||||||
return <AccessibleButton className="mx_UserInfo_field" onClick={onMuteToggle}>
|
return <AccessibleButton className={classes} onClick={onMuteToggle}>
|
||||||
{ muteLabel }
|
{ muteLabel }
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const RoomAdminToolsContainer = withLegacyMatrixClient(
|
const RoomAdminToolsContainer = withLegacyMatrixClient(
|
||||||
({matrixClient: cli, room, children, member, startUpdating, stopUpdating}) => {
|
({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => {
|
||||||
let kickButton;
|
let kickButton;
|
||||||
let banButton;
|
let banButton;
|
||||||
let muteButton;
|
let muteButton;
|
||||||
let redactButton;
|
let redactButton;
|
||||||
|
|
||||||
const powerLevels = useRoomPowerLevels(room);
|
|
||||||
const editPowerLevel = (
|
const editPowerLevel = (
|
||||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||||
powerLevels.state_default
|
powerLevels.state_default
|
||||||
|
@ -705,7 +743,7 @@ const GroupAdminToolsSection = withLegacyMatrixClient(
|
||||||
};
|
};
|
||||||
|
|
||||||
const kickButton = (
|
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') }
|
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -744,47 +782,17 @@ const useIsSynapseAdmin = (cli) => {
|
||||||
return isAdmin;
|
return isAdmin;
|
||||||
};
|
};
|
||||||
|
|
||||||
// cli is injected by withLegacyMatrixClient
|
function useRoomPermissions(cli, room, user) {
|
||||||
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]);
|
|
||||||
|
|
||||||
const [roomPermissions, setRoomPermissions] = useState({
|
const [roomPermissions, setRoomPermissions] = useState({
|
||||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||||
modifyLevelMax: -1,
|
modifyLevelMax: -1,
|
||||||
|
canEdit: false,
|
||||||
canInvite: false,
|
canInvite: false,
|
||||||
});
|
});
|
||||||
const updateRoomPermissions = useCallback(async () => {
|
const updateRoomPermissions = useCallback(() => {
|
||||||
if (!room) return;
|
if (!room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||||
if (!powerLevelEvent) return;
|
if (!powerLevelEvent) return;
|
||||||
|
@ -811,20 +819,197 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
|
|
||||||
setRoomPermissions({
|
setRoomPermissions({
|
||||||
canInvite: me.powerLevel >= powerLevels.invite,
|
canInvite: me.powerLevel >= powerLevels.invite,
|
||||||
|
canEdit: modifyLevelMax >= 0,
|
||||||
modifyLevelMax,
|
modifyLevelMax,
|
||||||
});
|
});
|
||||||
}, [cli, user, room]);
|
}, [cli, user, room]);
|
||||||
useEventEmitter(cli, "RoomState.events", updateRoomPermissions);
|
useEventEmitter(cli, "RoomState.members", updateRoomPermissions);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateRoomPermissions();
|
updateRoomPermissions();
|
||||||
return () => {
|
return () => {
|
||||||
setRoomPermissions({
|
setRoomPermissions({
|
||||||
maximalPowerLevel: -1,
|
maximalPowerLevel: -1,
|
||||||
|
canEdit: false,
|
||||||
canInvite: false,
|
canInvite: false,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [updateRoomPermissions]);
|
}, [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 onSynapseDeactivate = useCallback(async () => {
|
||||||
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
|
||||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||||
|
@ -852,71 +1037,13 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
}
|
}
|
||||||
}, [cli, user.userId]);
|
}, [cli, user.userId]);
|
||||||
|
|
||||||
const onPowerChange = useCallback(async (powerLevel) => {
|
|
||||||
const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
const onMemberAvatarKey = e => {
|
||||||
startUpdating();
|
if (e.key === "Enter") {
|
||||||
cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
onMemberAvatarClick();
|
||||||
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 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 onMemberAvatarClick = useCallback(() => {
|
||||||
const member = user;
|
const member = user;
|
||||||
const avatarUrl = member.getMxcAvatarUrl();
|
const avatarUrl = member.getMxcAvatarUrl();
|
||||||
|
@ -935,17 +1062,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
let synapseDeactivateButton;
|
let synapseDeactivateButton;
|
||||||
let spinner;
|
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
|
// 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.
|
// 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
|
// FIXME this should be using cli instead of MatrixClientPeg.matrixClient
|
||||||
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
|
if (isSynapseAdmin && user.userId.endsWith(`:${MatrixClientPeg.getHomeserverName()}`)) {
|
||||||
synapseDeactivateButton = (
|
synapseDeactivateButton = (
|
||||||
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field">
|
<AccessibleButton onClick={onSynapseDeactivate} className="mx_UserInfo_field mx_UserInfo_destructive">
|
||||||
{_t("Deactivate user")}
|
{_t("Deactivate user")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -955,6 +1077,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
if (room && user.roomId) {
|
if (room && user.roomId) {
|
||||||
adminToolsContainer = (
|
adminToolsContainer = (
|
||||||
<RoomAdminToolsContainer
|
<RoomAdminToolsContainer
|
||||||
|
powerLevels={powerLevels}
|
||||||
member={user}
|
member={user}
|
||||||
room={room}
|
room={room}
|
||||||
startUpdating={startUpdating}
|
startUpdating={startUpdating}
|
||||||
|
@ -992,7 +1115,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
let presenceCurrentlyActive;
|
let presenceCurrentlyActive;
|
||||||
let statusMessage;
|
let statusMessage;
|
||||||
|
|
||||||
if (user instanceof RoomMember) {
|
if (user instanceof RoomMember && user.user) {
|
||||||
presenceState = user.user.presence;
|
presenceState = user.user.presence;
|
||||||
presenceLastActiveAgo = user.user.lastActiveAgo;
|
presenceLastActiveAgo = user.user.lastActiveAgo;
|
||||||
presenceCurrentlyActive = user.user.currentlyActive;
|
presenceCurrentlyActive = user.user.currentlyActive;
|
||||||
|
@ -1021,32 +1144,19 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
statusLabel = <span className="mx_UserInfo_statusMessage">{ statusMessage }</span>;
|
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;
|
const avatarUrl = user.getMxcAvatarUrl ? user.getMxcAvatarUrl() : user.avatarUrl;
|
||||||
let avatarElement;
|
let avatarElement;
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
|
const httpUrl = cli.mxcUrlToHttp(avatarUrl, 800, 800);
|
||||||
avatarElement = <div className="mx_UserInfo_avatar" onClick={onMemberAvatarClick}>
|
avatarElement = <div
|
||||||
<img src={httpUrl} alt={_t("Profile picture")} />
|
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>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1058,6 +1168,12 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
title={_t('Close')} />;
|
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
|
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||||
const [devices, setDevices] = useState(undefined);
|
const [devices, setDevices] = useState(undefined);
|
||||||
// Download device lists
|
// Download device lists
|
||||||
|
@ -1082,14 +1198,15 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
setDevices(null);
|
setDevices(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isRoomEncrypted) {
|
||||||
_downloadDeviceList();
|
_downloadDeviceList();
|
||||||
|
}
|
||||||
|
|
||||||
// Handle being unmounted
|
// Handle being unmounted
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [cli, user.userId]);
|
}, [cli, user.userId, isRoomEncrypted]);
|
||||||
|
|
||||||
// Listen to changes
|
// Listen to changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1106,21 +1223,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isRoomEncrypted) {
|
||||||
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
cli.on("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||||
|
}
|
||||||
// Handle being unmounted
|
// Handle being unmounted
|
||||||
return () => {
|
return () => {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
|
||||||
};
|
|
||||||
}, [cli, user.userId]);
|
|
||||||
|
|
||||||
let devicesSection;
|
|
||||||
const isRoomEncrypted = _enableDevices && room && cli.isRoomEncrypted(room.roomId);
|
|
||||||
if (isRoomEncrypted) {
|
if (isRoomEncrypted) {
|
||||||
devicesSection = <DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />;
|
cli.removeListener("deviceVerificationChanged", onDeviceVerificationChanged);
|
||||||
} else {
|
}
|
||||||
let text;
|
};
|
||||||
|
}, [cli, user.userId, isRoomEncrypted]);
|
||||||
|
|
||||||
|
let text;
|
||||||
|
if (!isRoomEncrypted) {
|
||||||
if (!_enableDevices) {
|
if (!_enableDevices) {
|
||||||
text = _t("This client does not support end-to-end encryption.");
|
text = _t("This client does not support end-to-end encryption.");
|
||||||
} else if (room) {
|
} else if (room) {
|
||||||
|
@ -1128,22 +1244,24 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
} else {
|
} else {
|
||||||
// TODO what to render for GroupMember
|
// TODO what to render for GroupMember
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
text = _t("Messages in this room are end-to-end encrypted.");
|
||||||
|
}
|
||||||
|
|
||||||
if (text) {
|
const devicesSection = isRoomEncrypted ?
|
||||||
devicesSection = (
|
(<DevicesSection loading={devices === undefined} devices={devices} userId={user.userId} />) : null;
|
||||||
|
const securitySection = (
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
<h3>{ _t("Trust & Devices") }</h3>
|
<h3>{ _t("Security") }</h3>
|
||||||
<div className="mx_UserInfo_devices">
|
<p>{ text }</p>
|
||||||
{ text }
|
<AccessibleButton className="mx_UserInfo_verify" onClick={() => verifyDevice(user.userId, null)}>{_t("Verify")}</AccessibleButton>
|
||||||
</div>
|
{ devicesSection }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let e2eIcon;
|
let e2eIcon;
|
||||||
if (isRoomEncrypted && devices) {
|
if (isRoomEncrypted && devices) {
|
||||||
e2eIcon = <E2EIcon status={_getE2EStatus(devices)} isUser={true} />;
|
e2eIcon = <E2EIcon size={18} status={_getE2EStatus(devices)} isUser={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1153,16 +1271,14 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
|
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
<div className="mx_UserInfo_profile">
|
<div className="mx_UserInfo_profile">
|
||||||
<div className="mx_UserInfo_profileField">
|
<div >
|
||||||
<h2>
|
<h2 aria-label={displayName}>
|
||||||
{ e2eIcon }
|
{ e2eIcon }
|
||||||
{ displayName }
|
{ displayName }
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserInfo_profileField">
|
<div>{ user.userId }</div>
|
||||||
{ user.userId }
|
<div className="mx_UserInfo_profileStatus">
|
||||||
</div>
|
|
||||||
<div className="mx_UserInfo_profileField">
|
|
||||||
{presenceLabel}
|
{presenceLabel}
|
||||||
{statusLabel}
|
{statusLabel}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1176,11 +1292,9 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
|
||||||
</div> }
|
</div> }
|
||||||
|
|
||||||
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
|
||||||
{ devicesSection }
|
{ securitySection }
|
||||||
|
|
||||||
{ directChatsSection }
|
|
||||||
|
|
||||||
<UserOptionsSection
|
<UserOptionsSection
|
||||||
|
devices={devices}
|
||||||
canInvite={roomPermissions.canInvite}
|
canInvite={roomPermissions.canInvite}
|
||||||
isIgnored={isIgnored}
|
isIgnored={isIgnored}
|
||||||
member={user} />
|
member={user} />
|
||||||
|
|
|
@ -36,7 +36,13 @@ export default function(props) {
|
||||||
_t("All devices for this user are trusted") :
|
_t("All devices for this user are trusted") :
|
||||||
_t("All devices in this encrypted room 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) {
|
if (props.onClick) {
|
||||||
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
return (<AccessibleButton onClick={props.onClick}>{ icon }</AccessibleButton>);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -606,8 +606,8 @@ module.exports = createReactClass({
|
||||||
mx_EventTile_last: this.props.last,
|
mx_EventTile_last: this.props.last,
|
||||||
mx_EventTile_contextual: this.props.contextual,
|
mx_EventTile_contextual: this.props.contextual,
|
||||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||||
mx_EventTile_verified: this.state.verified === true,
|
mx_EventTile_verified: !isBubbleMessage && this.state.verified === true,
|
||||||
mx_EventTile_unverified: this.state.verified === false,
|
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === false,
|
||||||
mx_EventTile_bad: isEncryptionFailure,
|
mx_EventTile_bad: isEncryptionFailure,
|
||||||
mx_EventTile_emote: msgtype === 'm.emote',
|
mx_EventTile_emote: msgtype === 'm.emote',
|
||||||
mx_EventTile_redacted: isRedacted,
|
mx_EventTile_redacted: isRedacted,
|
||||||
|
@ -800,7 +800,7 @@ module.exports = createReactClass({
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ this._renderE2EPadlock() }
|
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||||
{ thread }
|
{ thread }
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
|
@ -826,7 +826,7 @@ module.exports = createReactClass({
|
||||||
{ readAvatars }
|
{ readAvatars }
|
||||||
</div>
|
</div>
|
||||||
{ sender }
|
{ sender }
|
||||||
<div className={classNames("mx_EventTile_line", {mx_EventTile_bubbleLine: isBubbleMessage})}>
|
<div className="mx_EventTile_line">
|
||||||
<a
|
<a
|
||||||
href={permalink}
|
href={permalink}
|
||||||
onClick={this.onPermalinkClicked}
|
onClick={this.onPermalinkClicked}
|
||||||
|
@ -834,7 +834,7 @@ module.exports = createReactClass({
|
||||||
>
|
>
|
||||||
{ timestamp }
|
{ timestamp }
|
||||||
</a>
|
</a>
|
||||||
{ this._renderE2EPadlock() }
|
{ !isBubbleMessage && this._renderE2EPadlock() }
|
||||||
{ thread }
|
{ thread }
|
||||||
<EventTileType ref="tile"
|
<EventTileType ref="tile"
|
||||||
mxEvent={this.props.mxEvent}
|
mxEvent={this.props.mxEvent}
|
||||||
|
|
|
@ -118,6 +118,7 @@
|
||||||
"Restricted": "Restricted",
|
"Restricted": "Restricted",
|
||||||
"Moderator": "Moderator",
|
"Moderator": "Moderator",
|
||||||
"Admin": "Admin",
|
"Admin": "Admin",
|
||||||
|
"Custom (%(level)s)": "Custom (%(level)s)",
|
||||||
"Start a chat": "Start a chat",
|
"Start a chat": "Start a chat",
|
||||||
"Who would you like to communicate with?": "Who would you like to communicate with?",
|
"Who would you like to communicate with?": "Who would you like to communicate with?",
|
||||||
"Email, name or Matrix ID": "Email, name or Matrix ID",
|
"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.",
|
"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",
|
"Members": "Members",
|
||||||
"Files": "Files",
|
"Files": "Files",
|
||||||
"Trust & Devices": "Trust & Devices",
|
"Trusted": "Trusted",
|
||||||
"Direct messages": "Direct messages",
|
"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",
|
"Remove from community": "Remove from community",
|
||||||
"Disinvite this user from community?": "Disinvite this user from community?",
|
"Disinvite this user from community?": "Disinvite this user from community?",
|
||||||
"Remove this user from community?": "Remove this user from community?",
|
"Remove this user from community?": "Remove this user from community?",
|
||||||
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
"Failed to withdraw invitation": "Failed to withdraw invitation",
|
||||||
"Failed to remove user from community": "Failed to remove user from community",
|
"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",
|
"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.",
|
"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 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",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
"Tuesday": "Tuesday",
|
"Tuesday": "Tuesday",
|
||||||
|
@ -1091,7 +1102,6 @@
|
||||||
"Reply": "Reply",
|
"Reply": "Reply",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Message Actions": "Message Actions",
|
"Message Actions": "Message Actions",
|
||||||
"Options": "Options",
|
|
||||||
"Attachment": "Attachment",
|
"Attachment": "Attachment",
|
||||||
"Error decrypting attachment": "Error decrypting attachment",
|
"Error decrypting attachment": "Error decrypting attachment",
|
||||||
"Decrypt %(text)s": "Decrypt %(text)s",
|
"Decrypt %(text)s": "Decrypt %(text)s",
|
||||||
|
|
Loading…
Reference in a new issue