Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into joriks/room-list-auto-expand-on-search
This commit is contained in:
commit
9b79de7fe7
63 changed files with 2300 additions and 799 deletions
|
@ -89,11 +89,11 @@
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"qs": "^6.6.0",
|
"qs": "^6.6.0",
|
||||||
|
"re-resizable": "^6.5.2",
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^4.0.1",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.9.0",
|
||||||
"react-focus-lock": "^2.2.1",
|
"react-focus-lock": "^2.2.1",
|
||||||
"react-resizable": "^1.10.1",
|
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.0",
|
"resize-observer-polyfill": "^1.5.0",
|
||||||
"sanitize-html": "^1.18.4",
|
"sanitize-html": "^1.18.4",
|
||||||
|
@ -122,6 +122,7 @@
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
|
"@types/linkifyjs": "^2.1.3",
|
||||||
"@types/lodash": "^4.14.152",
|
"@types/lodash": "^4.14.152",
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/node": "^12.12.41",
|
"@types/node": "^12.12.41",
|
||||||
|
@ -129,6 +130,7 @@
|
||||||
"@types/react": "^16.9",
|
"@types/react": "^16.9",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
|
"@types/sanitize-html": "^1.23.3",
|
||||||
"@types/zxcvbn": "^4.4.0",
|
"@types/zxcvbn": "^4.4.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
@import "./views/avatars/_BaseAvatar.scss";
|
@import "./views/avatars/_BaseAvatar.scss";
|
||||||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||||
|
@import "./views/avatars/_PulsedAvatar.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_RoomTileContextMenu.scss";
|
@import "./views/context_menus/_RoomTileContextMenu.scss";
|
||||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||||
|
@ -225,6 +226,8 @@
|
||||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
|
@import "./views/voip/_CallView2.scss";
|
||||||
@import "./views/voip/_IncomingCallbox.scss";
|
@import "./views/voip/_IncomingCallbox.scss";
|
||||||
@import "./views/voip/_VideoView.scss";
|
@import "./views/voip/_VideoView.scss";
|
||||||
|
|
|
@ -121,6 +121,24 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanel2_roomListWrapper {
|
||||||
|
// Create a flexbox to ensure the containing items cause appropriate overflow.
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 12px; // so we're not up against the search/filter
|
||||||
|
|
||||||
|
&.mx_LeftPanel2_roomListWrapper_stickyBottom {
|
||||||
|
padding-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_LeftPanel2_roomListWrapper_stickyTop {
|
||||||
|
padding-top: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel2_actualRoomListContainer {
|
.mx_LeftPanel2_actualRoomListContainer {
|
||||||
flex-grow: 1; // fill the available space
|
flex-grow: 1; // fill the available space
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
30
res/css/views/avatars/_PulsedAvatar.scss
Normal file
30
res/css/views/avatars/_PulsedAvatar.scss
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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_PulsedAvatar {
|
||||||
|
@keyframes shadow-pulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0px rgba($accent-color, 0.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 6px rgba($accent-color, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
animation: shadow-pulse 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,11 @@ limitations under the License.
|
||||||
// with text-align in parent
|
// with text-align in parent
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
|
color: $roomtile-badge-fg-color;
|
||||||
|
background-color: $roomtile-name-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge {
|
||||||
color: $secondary-accent-color;
|
color: $secondary-accent-color;
|
||||||
background-color: $warning-color;
|
background-color: $warning-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,6 @@ limitations under the License.
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 12px; // so we're not up against the search/filter
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSublist2_headerContainer {
|
.mx_RoomSublist2_headerContainer {
|
||||||
// Create a flexbox to make alignment easy
|
// Create a flexbox to make alignment easy
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,13 +45,15 @@ limitations under the License.
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
||||||
|
// Hide the header container if the contained element is stickied.
|
||||||
|
// We don't use display:none as that causes the header to go away too.
|
||||||
|
&.mx_RoomSublist2_headerContainer_hasSticky {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist2_stickable {
|
.mx_RoomSublist2_stickable {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
z-index: 2; // Prioritize headers in the visible list over sticky ones
|
|
||||||
|
|
||||||
// Set the same background color as the room list for sticky headers
|
|
||||||
background-color: $roomlist2-bg-color;
|
|
||||||
|
|
||||||
// Create a flexbox to make ordering easy
|
// Create a flexbox to make ordering easy
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -67,7 +65,6 @@ limitations under the License.
|
||||||
// when sticky scrolls instead of collapses the list.
|
// when sticky scrolls instead of collapses the list.
|
||||||
&.mx_RoomSublist2_headerContainer_sticky {
|
&.mx_RoomSublist2_headerContainer_sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1; // over top of other elements, but still under the ones in the visible list
|
|
||||||
height: 32px; // to match the header container
|
height: 32px; // to match the header container
|
||||||
// width set by JS
|
// width set by JS
|
||||||
}
|
}
|
||||||
|
@ -190,28 +187,29 @@ limitations under the License.
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.mx_RoomSublist2_placeholder {
|
||||||
|
height: 44px; // Height of a room tile plus margins
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomSublist2_showNButton {
|
.mx_RoomSublist2_showNButton {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: $font-13px;
|
font-size: $font-13px;
|
||||||
line-height: $font-18px;
|
line-height: $font-18px;
|
||||||
color: $roomtile2-preview-color;
|
color: $roomtile2-preview-color;
|
||||||
|
|
||||||
// This is the same color as the left panel background because it needs
|
|
||||||
// to occlude the lastmost tile in the list.
|
|
||||||
background-color: $roomlist2-bg-color;
|
|
||||||
|
|
||||||
// Update the render() function for RoomSublist2 if these change
|
// Update the render() function for RoomSublist2 if these change
|
||||||
// Update the ListLayout class for minVisibleTiles if these change.
|
// Update the ListLayout class for minVisibleTiles if these change.
|
||||||
//
|
//
|
||||||
// At 24px high and 8px padding on the top this equates to 0.65 of
|
// At 24px high, 8px padding on the top and 4px padding on the bottom this equates to 0.73 of
|
||||||
// a tile due to how the padding calculations work.
|
// a tile due to how the padding calculations work.
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
|
||||||
// We force this to the bottom so it will overlap rooms as needed.
|
// We force this to the bottom so it will overlap rooms as needed.
|
||||||
// We account for the space it takes up (24px) in the code through padding.
|
// We account for the space it takes up (24px) in the code through padding.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 4px; // the height of the resize handle
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
|
@ -238,39 +236,31 @@ limitations under the License.
|
||||||
.mx_RoomSublist2_showLessButtonChevron {
|
.mx_RoomSublist2_showLessButtonChevron {
|
||||||
mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
|
mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomSublist2_isCutting::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 4px;
|
|
||||||
box-shadow: 0px -2px 3px rgba(46, 47, 50, 0.08);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Class name comes from the ResizableBox component
|
// Class name comes from the ResizableBox component
|
||||||
// The hover state needs to use the whole sublist, not just the resizable box,
|
// The hover state needs to use the whole sublist, not just the resizable box,
|
||||||
// so that selector is below and one level higher.
|
// so that selector is below and one level higher.
|
||||||
.react-resizable-handle {
|
.mx_RoomSublist2_resizerHandle {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
// Update RESIZE_HANDLE_HEIGHT if this changes
|
// Override styles from library
|
||||||
height: 4px;
|
width: unset !important;
|
||||||
|
height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes
|
||||||
|
|
||||||
// This is positioned directly below the 'show more' button.
|
// This is positioned directly below the 'show more' button.
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0 !important; // override from library
|
||||||
|
|
||||||
// Together, these make the bar 64px wide
|
// Together, these make the bar 64px wide
|
||||||
left: calc(50% - 32px);
|
// These are also overridden from the library
|
||||||
right: calc(50% - 32px);
|
left: calc(50% - 32px) !important;
|
||||||
|
right: calc(50% - 32px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
&:hover, &.mx_RoomSublist2_hasMenuOpen {
|
||||||
.react-resizable-handle {
|
.mx_RoomSublist2_resizerHandle {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
background-color: $primary-fg-color;
|
background-color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,10 @@ limitations under the License.
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
|
// allow scrollIntoView to ignore the sticky headers, must match combined height of .mx_RoomSublist2_headerContainer
|
||||||
|
scroll-margin-top: 32px;
|
||||||
|
scroll-margin-bottom: 32px;
|
||||||
|
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -165,6 +169,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do not apply scroll-margin-bottom to the sublist which will not have a sticky header below it
|
||||||
|
.mx_RoomSublist2:last-child .mx_RoomTile2 {
|
||||||
|
scroll-margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
// We use these both in context menus and the room tiles
|
// We use these both in context menus and the room tiles
|
||||||
.mx_RoomTile2_iconBell::before {
|
.mx_RoomTile2_iconBell::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/bell.svg');
|
mask-image: url('$(res)/img/feather-customised/bell.svg');
|
||||||
|
@ -224,6 +233,10 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/feather-customised/star.svg');
|
mask-image: url('$(res)/img/feather-customised/star.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomTile2_iconFavorite::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/favourites.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_iconArrowDown::before {
|
.mx_RoomTile2_iconArrowDown::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/arrow-down.svg');
|
mask-image: url('$(res)/img/feather-customised/arrow-down.svg');
|
||||||
}
|
}
|
||||||
|
|
89
res/css/views/voip/_CallContainer.scss
Normal file
89
res/css/views/voip/_CallContainer.scss
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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_CallContainer {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 72px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.mx_CallPreview {
|
||||||
|
.mx_VideoView {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VideoView_localVideoFeed {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallBox2 {
|
||||||
|
min-width: 250px;
|
||||||
|
background-color: $primary-bg-color;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.mx_IncomingCallBox2_CallerInfo {
|
||||||
|
display: flex;
|
||||||
|
direction: row;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, p {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallBox2_buttons {
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
> .mx_IncomingCallBox2_spacer {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
res/css/views/voip/_CallView2.scss
Normal file
96
res/css/views/voip/_CallView2.scss
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
.mx_CallView2_voice {
|
||||||
|
background-color: $accent-color;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// Hacky vertical align
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div > p,
|
||||||
|
> div > h1 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-13px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div > p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView2_hangup {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
right: 8px;
|
||||||
|
bottom: 10px;
|
||||||
|
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
|
||||||
|
border-radius: 35px;
|
||||||
|
|
||||||
|
background-color: $notice-primary-color;
|
||||||
|
|
||||||
|
z-index: 101;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
top: 6.5px;
|
||||||
|
left: 7.5px;
|
||||||
|
|
||||||
|
mask: url('$(res)/img/hangup.svg');
|
||||||
|
mask-size: contain;
|
||||||
|
background-size: contain;
|
||||||
|
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
3
res/img/feather-customised/favourites.svg
Normal file
3
res/img/feather-customised/favourites.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.41411 0.432179C7.59217 -0.144061 8.40783 -0.144059 8.58589 0.43218L10.1715 5.56319H15.3856C15.9721 5.56319 16.224 6.30764 15.7578 6.66373L11.5135 9.90611L13.1185 15.1001C13.2948 15.6705 12.6348 16.1309 12.1604 15.7684L8 12.5902L3.83965 15.7684C3.3652 16.1309 2.70521 15.6705 2.88148 15.1001L4.4865 9.90611L0.242159 6.66373C-0.223967 6.30764 0.0278507 5.56319 0.614427 5.56319H5.82854L7.41411 0.432179Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 533 B |
|
@ -17,3 +17,4 @@ limitations under the License.
|
||||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||||
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
||||||
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||||
|
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -21,6 +21,7 @@ import ToastStore from "../stores/ToastStore";
|
||||||
import DeviceListener from "../DeviceListener";
|
import DeviceListener from "../DeviceListener";
|
||||||
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
|
||||||
import { PlatformPeg } from "../PlatformPeg";
|
import { PlatformPeg } from "../PlatformPeg";
|
||||||
|
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -34,6 +35,7 @@ declare global {
|
||||||
mx_ToastStore: ToastStore;
|
mx_ToastStore: ToastStore;
|
||||||
mx_DeviceListener: DeviceListener;
|
mx_DeviceListener: DeviceListener;
|
||||||
mx_RoomListStore2: RoomListStore2;
|
mx_RoomListStore2: RoomListStore2;
|
||||||
|
mx_RoomListLayoutStore: RoomListLayoutStore;
|
||||||
mxPlatformPeg: PlatformPeg;
|
mxPlatformPeg: PlatformPeg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
38
src/@types/polyfill.ts
Normal file
38
src/@types/polyfill.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This is intended to fix re-resizer because of its unguarded `instanceof TouchEvent` checks.
|
||||||
|
export function polyfillTouchEvent() {
|
||||||
|
// Firefox doesn't have touch events without touch devices being present, so create a fake
|
||||||
|
// one we can rely on lying about.
|
||||||
|
if (!window.TouchEvent) {
|
||||||
|
// We have no intention of actually using this, so just lie.
|
||||||
|
window.TouchEvent = class TouchEvent extends UIEvent {
|
||||||
|
public get altKey(): boolean { return false; }
|
||||||
|
public get changedTouches(): any { return []; }
|
||||||
|
public get ctrlKey(): boolean { return false; }
|
||||||
|
public get metaKey(): boolean { return false; }
|
||||||
|
public get shiftKey(): boolean { return false; }
|
||||||
|
public get targetTouches(): any { return []; }
|
||||||
|
public get touches(): any { return []; }
|
||||||
|
public get rotation(): number { return 0.0; }
|
||||||
|
public get scale(): number { return 0.0; }
|
||||||
|
constructor(eventType: string, params?: any) {
|
||||||
|
super(eventType, params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,10 +17,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import * as linkify from 'linkifyjs';
|
import * as linkify from 'linkifyjs';
|
||||||
|
@ -28,12 +24,13 @@ import linkifyMatrix from './linkify-matrix';
|
||||||
import _linkifyElement from 'linkifyjs/element';
|
import _linkifyElement from 'linkifyjs/element';
|
||||||
import _linkifyString from 'linkifyjs/string';
|
import _linkifyString from 'linkifyjs/string';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
import EMOJIBASE_REGEX from 'emojibase-regex';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||||
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -64,7 +61,7 @@ const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||||
* need emojification.
|
* need emojification.
|
||||||
* unicodeToImage uses this function.
|
* unicodeToImage uses this function.
|
||||||
*/
|
*/
|
||||||
function mightContainEmoji(str) {
|
function mightContainEmoji(str: string) {
|
||||||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +71,7 @@ function mightContainEmoji(str) {
|
||||||
* @param {String} char The emoji character
|
* @param {String} char The emoji character
|
||||||
* @return {String} The shortcode (such as :thumbup:)
|
* @return {String} The shortcode (such as :thumbup:)
|
||||||
*/
|
*/
|
||||||
export function unicodeToShortcode(char) {
|
export function unicodeToShortcode(char: string) {
|
||||||
const data = getEmojiFromUnicode(char);
|
const data = getEmojiFromUnicode(char);
|
||||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||||
}
|
}
|
||||||
|
@ -85,7 +82,7 @@ export function unicodeToShortcode(char) {
|
||||||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
* @param {String} shortcode The shortcode (such as :thumbup:)
|
||||||
* @return {String} The emoji character; null if none exists
|
* @return {String} The emoji character; null if none exists
|
||||||
*/
|
*/
|
||||||
export function shortcodeToUnicode(shortcode) {
|
export function shortcodeToUnicode(shortcode: string) {
|
||||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||||
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
||||||
return data ? data.unicode : null;
|
return data ? data.unicode : null;
|
||||||
|
@ -122,12 +119,19 @@ export function processHtmlForSending(html: string): string {
|
||||||
* Given an untrusted HTML string, return a React node with an sanitized version
|
* Given an untrusted HTML string, return a React node with an sanitized version
|
||||||
* of that HTML.
|
* of that HTML.
|
||||||
*/
|
*/
|
||||||
export function sanitizedHtmlNode(insaneHtml) {
|
export function sanitizedHtmlNode(insaneHtml: string) {
|
||||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizedHtmlNodeInnerText(insaneHtml: string) {
|
||||||
|
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||||
|
const contentDiv = document.createElement("div");
|
||||||
|
contentDiv.innerHTML = saneHtml;
|
||||||
|
return contentDiv.innerText;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests if a URL from an untrusted source may be safely put into the DOM
|
* Tests if a URL from an untrusted source may be safely put into the DOM
|
||||||
* The biggest threat here is javascript: URIs.
|
* The biggest threat here is javascript: URIs.
|
||||||
|
@ -136,7 +140,7 @@ export function sanitizedHtmlNode(insaneHtml) {
|
||||||
* other places we need to sanitise URLs.
|
* other places we need to sanitise URLs.
|
||||||
* @return true if permitted, otherwise false
|
* @return true if permitted, otherwise false
|
||||||
*/
|
*/
|
||||||
export function isUrlPermitted(inputUrl) {
|
export function isUrlPermitted(inputUrl: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = url.parse(inputUrl);
|
const parsed = url.parse(inputUrl);
|
||||||
if (!parsed.protocol) return false;
|
if (!parsed.protocol) return false;
|
||||||
|
@ -147,9 +151,9 @@ export function isUrlPermitted(inputUrl) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformTags = { // custom to matrix
|
const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to matrix
|
||||||
// add blank targets to all hyperlinks except vector URLs
|
// add blank targets to all hyperlinks except vector URLs
|
||||||
'a': function(tagName, attribs) {
|
'a': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
if (attribs.href) {
|
if (attribs.href) {
|
||||||
attribs.target = '_blank'; // by default
|
attribs.target = '_blank'; // by default
|
||||||
|
|
||||||
|
@ -162,7 +166,7 @@ const transformTags = { // custom to matrix
|
||||||
attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'img': function(tagName, attribs) {
|
'img': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||||
// we don't want to allow images with `https?` `src`s.
|
// we don't want to allow images with `https?` `src`s.
|
||||||
|
@ -176,7 +180,7 @@ const transformTags = { // custom to matrix
|
||||||
);
|
);
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'code': function(tagName, attribs) {
|
'code': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
if (typeof attribs.class !== 'undefined') {
|
if (typeof attribs.class !== 'undefined') {
|
||||||
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
||||||
const classes = attribs.class.split(/\s/).filter(function(cl) {
|
const classes = attribs.class.split(/\s/).filter(function(cl) {
|
||||||
|
@ -186,7 +190,7 @@ const transformTags = { // custom to matrix
|
||||||
}
|
}
|
||||||
return { tagName, attribs };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'*': function(tagName, attribs) {
|
'*': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||||
// Delete any style previously assigned, style is an allowedTag for font and span
|
// Delete any style previously assigned, style is an allowedTag for font and span
|
||||||
// because attributes are stripped after transforming
|
// because attributes are stripped after transforming
|
||||||
delete attribs.style;
|
delete attribs.style;
|
||||||
|
@ -220,7 +224,7 @@ const transformTags = { // custom to matrix
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeHtmlParams = {
|
const sanitizeHtmlParams: sanitizeHtml.IOptions = {
|
||||||
allowedTags: [
|
allowedTags: [
|
||||||
'font', // custom to matrix for IRC-style font coloring
|
'font', // custom to matrix for IRC-style font coloring
|
||||||
'del', // for markdown
|
'del', // for markdown
|
||||||
|
@ -247,16 +251,16 @@ const sanitizeHtmlParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is the same as the above except with less rewriting
|
// this is the same as the above except with less rewriting
|
||||||
const composerSanitizeHtmlParams = Object.assign({}, sanitizeHtmlParams);
|
const composerSanitizeHtmlParams: sanitizeHtml.IOptions = {
|
||||||
composerSanitizeHtmlParams.transformTags = {
|
...sanitizeHtmlParams,
|
||||||
|
transformTags: {
|
||||||
'code': transformTags['code'],
|
'code': transformTags['code'],
|
||||||
'*': transformTags['*'],
|
'*': transformTags['*'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseHighlighter {
|
abstract class BaseHighlighter<T extends React.ReactNode> {
|
||||||
constructor(highlightClass, highlightLink) {
|
constructor(public highlightClass: string, public highlightLink: string) {
|
||||||
this.highlightClass = highlightClass;
|
|
||||||
this.highlightLink = highlightLink;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -270,47 +274,49 @@ class BaseHighlighter {
|
||||||
* returns a list of results (strings for HtmlHighligher, react nodes for
|
* returns a list of results (strings for HtmlHighligher, react nodes for
|
||||||
* TextHighlighter).
|
* TextHighlighter).
|
||||||
*/
|
*/
|
||||||
applyHighlights(safeSnippet, safeHighlights) {
|
public applyHighlights(safeSnippet: string, safeHighlights: string[]): T[] {
|
||||||
let lastOffset = 0;
|
let lastOffset = 0;
|
||||||
let offset;
|
let offset;
|
||||||
let nodes = [];
|
let nodes: T[] = [];
|
||||||
|
|
||||||
const safeHighlight = safeHighlights[0];
|
const safeHighlight = safeHighlights[0];
|
||||||
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
|
||||||
// handle preamble
|
// handle preamble
|
||||||
if (offset > lastOffset) {
|
if (offset > lastOffset) {
|
||||||
var subSnippet = safeSnippet.substring(lastOffset, offset);
|
const subSnippet = safeSnippet.substring(lastOffset, offset);
|
||||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
nodes = nodes.concat(this.applySubHighlights(subSnippet, safeHighlights));
|
||||||
}
|
}
|
||||||
|
|
||||||
// do highlight. use the original string rather than safeHighlight
|
// do highlight. use the original string rather than safeHighlight
|
||||||
// to preserve the original casing.
|
// to preserve the original casing.
|
||||||
const endOffset = offset + safeHighlight.length;
|
const endOffset = offset + safeHighlight.length;
|
||||||
nodes.push(this._processSnippet(safeSnippet.substring(offset, endOffset), true));
|
nodes.push(this.processSnippet(safeSnippet.substring(offset, endOffset), true));
|
||||||
|
|
||||||
lastOffset = endOffset;
|
lastOffset = endOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle postamble
|
// handle postamble
|
||||||
if (lastOffset !== safeSnippet.length) {
|
if (lastOffset !== safeSnippet.length) {
|
||||||
subSnippet = safeSnippet.substring(lastOffset, undefined);
|
const subSnippet = safeSnippet.substring(lastOffset, undefined);
|
||||||
nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
|
nodes = nodes.concat(this.applySubHighlights(subSnippet, safeHighlights));
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
_applySubHighlights(safeSnippet, safeHighlights) {
|
private applySubHighlights(safeSnippet: string, safeHighlights: string[]): T[] {
|
||||||
if (safeHighlights[1]) {
|
if (safeHighlights[1]) {
|
||||||
// recurse into this range to check for the next set of highlight matches
|
// recurse into this range to check for the next set of highlight matches
|
||||||
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
|
||||||
} else {
|
} else {
|
||||||
// no more highlights to be found, just return the unhighlighted string
|
// no more highlights to be found, just return the unhighlighted string
|
||||||
return [this._processSnippet(safeSnippet, false)];
|
return [this.processSnippet(safeSnippet, false)];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HtmlHighlighter extends BaseHighlighter {
|
protected abstract processSnippet(snippet: string, highlight: boolean): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HtmlHighlighter extends BaseHighlighter<string> {
|
||||||
/* highlight the given snippet if required
|
/* highlight the given snippet if required
|
||||||
*
|
*
|
||||||
* snippet: content of the span; must have been sanitised
|
* snippet: content of the span; must have been sanitised
|
||||||
|
@ -318,28 +324,23 @@ class HtmlHighlighter extends BaseHighlighter {
|
||||||
*
|
*
|
||||||
* returns an HTML string
|
* returns an HTML string
|
||||||
*/
|
*/
|
||||||
_processSnippet(snippet, highlight) {
|
protected processSnippet(snippet: string, highlight: boolean): string {
|
||||||
if (!highlight) {
|
if (!highlight) {
|
||||||
// nothing required here
|
// nothing required here
|
||||||
return snippet;
|
return snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let span = "<span class=\""+this.highlightClass+"\">"
|
let span = `<span class="${this.highlightClass}">${snippet}</span>`;
|
||||||
+ snippet + "</span>";
|
|
||||||
|
|
||||||
if (this.highlightLink) {
|
if (this.highlightLink) {
|
||||||
span = "<a href=\""+encodeURI(this.highlightLink)+"\">"
|
span = `<a href="${encodeURI(this.highlightLink)}">${span}</a>`;
|
||||||
+span+"</a>";
|
|
||||||
}
|
}
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextHighlighter extends BaseHighlighter {
|
class TextHighlighter extends BaseHighlighter<React.ReactNode> {
|
||||||
constructor(highlightClass, highlightLink) {
|
private key = 0;
|
||||||
super(highlightClass, highlightLink);
|
|
||||||
this._key = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* create a <span> node to hold the given content
|
/* create a <span> node to hold the given content
|
||||||
*
|
*
|
||||||
|
@ -348,11 +349,10 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
*
|
*
|
||||||
* returns a React node
|
* returns a React node
|
||||||
*/
|
*/
|
||||||
_processSnippet(snippet, highlight) {
|
protected processSnippet(snippet: string, highlight: boolean): React.ReactNode {
|
||||||
const key = this._key++;
|
const key = this.key++;
|
||||||
|
|
||||||
let node =
|
let node = <span key={key} className={highlight ? this.highlightClass : null}>
|
||||||
<span key={key} className={highlight ? this.highlightClass : null}>
|
|
||||||
{ snippet }
|
{ snippet }
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
|
@ -364,6 +364,20 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IContent {
|
||||||
|
format?: string;
|
||||||
|
formatted_body?: string;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
highlightLink?: string;
|
||||||
|
disableBigEmoji?: boolean;
|
||||||
|
stripReplyFallback?: boolean;
|
||||||
|
returnString?: boolean;
|
||||||
|
forComposerQuote?: boolean;
|
||||||
|
ref?: React.Ref<any>;
|
||||||
|
}
|
||||||
|
|
||||||
/* turn a matrix event body into html
|
/* turn a matrix event body into html
|
||||||
*
|
*
|
||||||
|
@ -378,7 +392,7 @@ class TextHighlighter extends BaseHighlighter {
|
||||||
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||||
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
|
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
|
||||||
*/
|
*/
|
||||||
export function bodyToHtml(content, highlights, opts={}) {
|
export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) {
|
||||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||||
let bodyHasEmoji = false;
|
let bodyHasEmoji = false;
|
||||||
|
|
||||||
|
@ -387,9 +401,9 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
sanitizeParams = composerSanitizeHtmlParams;
|
sanitizeParams = composerSanitizeHtmlParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
let strippedBody;
|
let strippedBody: string;
|
||||||
let safeBody;
|
let safeBody: string;
|
||||||
let isDisplayedWithHtml;
|
let isDisplayedWithHtml: boolean;
|
||||||
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
|
||||||
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
|
||||||
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
|
||||||
|
@ -471,7 +485,7 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||||
* @returns {string} Linkified string
|
* @returns {string} Linkified string
|
||||||
*/
|
*/
|
||||||
export function linkifyString(str, options = linkifyMatrix.options) {
|
export function linkifyString(str: string, options = linkifyMatrix.options) {
|
||||||
return _linkifyString(str, options);
|
return _linkifyString(str, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,7 +496,7 @@ export function linkifyString(str, options = linkifyMatrix.options) {
|
||||||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
|
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
export function linkifyElement(element, options = linkifyMatrix.options) {
|
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) {
|
||||||
return _linkifyElement(element, options);
|
return _linkifyElement(element, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,7 +507,7 @@ export function linkifyElement(element, options = linkifyMatrix.options) {
|
||||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) {
|
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) {
|
||||||
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,7 +518,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.option
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @returns {bool}
|
* @returns {bool}
|
||||||
*/
|
*/
|
||||||
export function checkBlockNode(node) {
|
export function checkBlockNode(node: Node) {
|
||||||
switch (node.nodeName) {
|
switch (node.nodeName) {
|
||||||
case "H1":
|
case "H1":
|
||||||
case "H2":
|
case "H2":
|
51
src/accessibility/context_menu/ContextMenuButton.tsx
Normal file
51
src/accessibility/context_menu/ContextMenuButton.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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 AccessibleButton, {IProps as IAccessibleButtonProps} from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends IAccessibleButtonProps {
|
||||||
|
label?: string;
|
||||||
|
// whether or not the context menu is currently open
|
||||||
|
isExpanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
||||||
|
export const ContextMenuButton: React.FC<IProps> = ({
|
||||||
|
label,
|
||||||
|
isExpanded,
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
onContextMenu,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
|
onClick={onClick}
|
||||||
|
onContextMenu={onContextMenu || onClick}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
aria-haspopup={true}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
30
src/accessibility/context_menu/MenuGroup.tsx
Normal file
30
src/accessibility/context_menu/MenuGroup.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface IProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=group for grouping menu radios/checkboxes
|
||||||
|
export const MenuGroup: React.FC<IProps> = ({children, label, ...props}) => {
|
||||||
|
return <div {...props} role="group" aria-label={label}>
|
||||||
|
{ children }
|
||||||
|
</div>;
|
||||||
|
};
|
35
src/accessibility/context_menu/MenuItem.tsx
Normal file
35
src/accessibility/context_menu/MenuItem.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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 AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=menuitem
|
||||||
|
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
43
src/accessibility/context_menu/MenuItemCheckbox.tsx
Normal file
43
src/accessibility/context_menu/MenuItemCheckbox.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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 AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
label?: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=menuitemcheckbox
|
||||||
|
export const MenuItemCheckbox: React.FC<IProps> = ({children, label, active, disabled, ...props}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
aria-checked={active}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
43
src/accessibility/context_menu/MenuItemRadio.tsx
Normal file
43
src/accessibility/context_menu/MenuItemRadio.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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 AccessibleButton from "../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
|
label?: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a role=menuitemradio
|
||||||
|
export const MenuItemRadio: React.FC<IProps> = ({children, label, active, disabled, ...props}) => {
|
||||||
|
return (
|
||||||
|
<AccessibleButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={active}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
};
|
64
src/accessibility/context_menu/StyledMenuItemCheckbox.tsx
Normal file
64
src/accessibility/context_menu/StyledMenuItemCheckbox.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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 {Key} from "../../Keyboard";
|
||||||
|
import StyledCheckbox from "../../components/views/elements/StyledCheckbox";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
||||||
|
label?: string;
|
||||||
|
onChange(); // we handle keyup/down ourselves so lose the ChangeEvent
|
||||||
|
onClose(): void; // gets called after onChange on Key.ENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemcheckbox
|
||||||
|
export const StyledMenuItemCheckbox: React.FC<IProps> = ({children, label, onChange, onClose, ...props}) => {
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledCheckbox
|
||||||
|
{...props}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledCheckbox>
|
||||||
|
);
|
||||||
|
};
|
64
src/accessibility/context_menu/StyledMenuItemRadio.tsx
Normal file
64
src/accessibility/context_menu/StyledMenuItemRadio.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
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 {Key} from "../../Keyboard";
|
||||||
|
import StyledRadioButton from "../../components/views/elements/StyledRadioButton";
|
||||||
|
|
||||||
|
interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
|
||||||
|
label?: string;
|
||||||
|
onChange(); // we handle keyup/down ourselves so lose the ChangeEvent
|
||||||
|
onClose(): void; // gets called after onChange on Key.ENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic component for representing a styled role=menuitemradio
|
||||||
|
export const StyledMenuItemRadio: React.FC<IProps> = ({children, label, onChange, onClose, ...props}) => {
|
||||||
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
onChange();
|
||||||
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
if (e.key === Key.ENTER) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
|
// prevent the input default handler as we handle it on keydown to match
|
||||||
|
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
||||||
|
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<StyledRadioButton
|
||||||
|
{...props}
|
||||||
|
role="menuitemradio"
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={label}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</StyledRadioButton>
|
||||||
|
);
|
||||||
|
};
|
|
@ -16,15 +16,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useRef, useState} from 'react';
|
import React, {CSSProperties, useRef, useState} from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import PropTypes from 'prop-types';
|
import classNames from "classnames";
|
||||||
import classNames from 'classnames';
|
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import * as sdk from "../../index";
|
import {Writeable} from "../../@types/common";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
|
||||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
|
||||||
import StyledRadioButton from "../views/elements/StyledRadioButton";
|
|
||||||
|
|
||||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||||
|
@ -32,8 +29,8 @@ import StyledRadioButton from "../views/elements/StyledRadioButton";
|
||||||
|
|
||||||
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
const ContextualMenuContainerId = "mx_ContextualMenu_Container";
|
||||||
|
|
||||||
function getOrCreateContainer() {
|
function getOrCreateContainer(): HTMLDivElement {
|
||||||
let container = document.getElementById(ContextualMenuContainerId);
|
let container = document.getElementById(ContextualMenuContainerId) as HTMLDivElement;
|
||||||
|
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = document.createElement("div");
|
container = document.createElement("div");
|
||||||
|
@ -45,50 +42,70 @@ function getOrCreateContainer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]);
|
||||||
|
|
||||||
|
interface IPosition {
|
||||||
|
top?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
right?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChevronFace {
|
||||||
|
Top = "top",
|
||||||
|
Bottom = "bottom",
|
||||||
|
Left = "left",
|
||||||
|
Right = "right",
|
||||||
|
None = "none",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps extends IPosition {
|
||||||
|
menuWidth?: number;
|
||||||
|
menuHeight?: number;
|
||||||
|
|
||||||
|
chevronOffset?: number;
|
||||||
|
chevronFace?: ChevronFace;
|
||||||
|
|
||||||
|
menuPaddingTop?: number;
|
||||||
|
menuPaddingBottom?: number;
|
||||||
|
menuPaddingLeft?: number;
|
||||||
|
menuPaddingRight?: number;
|
||||||
|
|
||||||
|
zIndex?: number;
|
||||||
|
|
||||||
|
// If true, insert an invisible screen-sized element behind the menu that when clicked will close it.
|
||||||
|
hasBackground?: boolean;
|
||||||
|
// whether this context menu should be focus managed. If false it must handle itself
|
||||||
|
managed?: boolean;
|
||||||
|
|
||||||
|
// Function to be called on menu close
|
||||||
|
onFinished();
|
||||||
|
// on resize callback
|
||||||
|
windowResize?();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
contextMenuElem: HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
// Generic ContextMenu Portal wrapper
|
// Generic ContextMenu Portal wrapper
|
||||||
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
|
||||||
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
|
||||||
export class ContextMenu extends React.Component {
|
export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
static propTypes = {
|
private initialFocus: HTMLElement;
|
||||||
top: PropTypes.number,
|
|
||||||
bottom: PropTypes.number,
|
|
||||||
left: PropTypes.number,
|
|
||||||
right: PropTypes.number,
|
|
||||||
menuWidth: PropTypes.number,
|
|
||||||
menuHeight: PropTypes.number,
|
|
||||||
chevronOffset: PropTypes.number,
|
|
||||||
chevronFace: PropTypes.string, // top, bottom, left, right or none
|
|
||||||
// Function to be called on menu close
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
menuPaddingTop: PropTypes.number,
|
|
||||||
menuPaddingRight: PropTypes.number,
|
|
||||||
menuPaddingBottom: PropTypes.number,
|
|
||||||
menuPaddingLeft: PropTypes.number,
|
|
||||||
zIndex: PropTypes.number,
|
|
||||||
|
|
||||||
// If true, insert an invisible screen-sized element behind the
|
|
||||||
// menu that when clicked will close it.
|
|
||||||
hasBackground: PropTypes.bool,
|
|
||||||
|
|
||||||
// on resize callback
|
|
||||||
windowResize: PropTypes.func,
|
|
||||||
|
|
||||||
managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasBackground: true,
|
hasBackground: true,
|
||||||
managed: true,
|
managed: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor(props, context) {
|
||||||
super();
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
contextMenuElem: null,
|
contextMenuElem: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// persist what had focus when we got initialized so we can return it after
|
// persist what had focus when we got initialized so we can return it after
|
||||||
this.initialFocus = document.activeElement;
|
this.initialFocus = document.activeElement as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -96,7 +113,7 @@ export class ContextMenu extends React.Component {
|
||||||
this.initialFocus.focus();
|
this.initialFocus.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
collectContextMenuRect = (element) => {
|
private collectContextMenuRect = (element) => {
|
||||||
// We don't need to clean up when unmounting, so ignore
|
// We don't need to clean up when unmounting, so ignore
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
|
@ -113,7 +130,7 @@ export class ContextMenu extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onContextMenu = (e) => {
|
private onContextMenu = (e) => {
|
||||||
if (this.props.onFinished) {
|
if (this.props.onFinished) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
|
||||||
|
@ -136,20 +153,20 @@ export class ContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onContextMenuPreventBubbling = (e) => {
|
private onContextMenuPreventBubbling = (e) => {
|
||||||
// stop propagation so that any context menu handlers don't leak out of this context menu
|
// stop propagation so that any context menu handlers don't leak out of this context menu
|
||||||
// but do not inhibit the default browser menu
|
// but do not inhibit the default browser menu
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent clicks on the background from going through to the component which opened the menu.
|
// Prevent clicks on the background from going through to the component which opened the menu.
|
||||||
_onFinished = (ev: InputEvent) => {
|
private onFinished = (ev: React.MouseEvent) => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (this.props.onFinished) this.props.onFinished();
|
if (this.props.onFinished) this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMoveFocus = (element, up) => {
|
private onMoveFocus = (element: Element, up: boolean) => {
|
||||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
@ -183,25 +200,25 @@ export class ContextMenu extends React.Component {
|
||||||
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
} while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role")));
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
(element as HTMLElement).focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onMoveFocusHomeEnd = (element, up) => {
|
private onMoveFocusHomeEnd = (element: Element, up: boolean) => {
|
||||||
let results = element.querySelectorAll('[role^="menuitem"]');
|
let results = element.querySelectorAll('[role^="menuitem"]');
|
||||||
if (!results) {
|
if (!results) {
|
||||||
results = element.querySelectorAll('[tab-index]');
|
results = element.querySelectorAll('[tab-index]');
|
||||||
}
|
}
|
||||||
if (results && results.length) {
|
if (results && results.length) {
|
||||||
if (up) {
|
if (up) {
|
||||||
results[0].focus();
|
(results[0] as HTMLElement).focus();
|
||||||
} else {
|
} else {
|
||||||
results[results.length - 1].focus();
|
(results[results.length - 1] as HTMLElement).focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (ev) => {
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
if (!this.props.managed) {
|
if (!this.props.managed) {
|
||||||
if (ev.key === Key.ESCAPE) {
|
if (ev.key === Key.ESCAPE) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
|
@ -219,16 +236,16 @@ export class ContextMenu extends React.Component {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
break;
|
break;
|
||||||
case Key.ARROW_UP:
|
case Key.ARROW_UP:
|
||||||
this._onMoveFocus(ev.target, true);
|
this.onMoveFocus(ev.target as Element, true);
|
||||||
break;
|
break;
|
||||||
case Key.ARROW_DOWN:
|
case Key.ARROW_DOWN:
|
||||||
this._onMoveFocus(ev.target, false);
|
this.onMoveFocus(ev.target as Element, false);
|
||||||
break;
|
break;
|
||||||
case Key.HOME:
|
case Key.HOME:
|
||||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
this.onMoveFocusHomeEnd(this.state.contextMenuElem, true);
|
||||||
break;
|
break;
|
||||||
case Key.END:
|
case Key.END:
|
||||||
this._onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
this.onMoveFocusHomeEnd(this.state.contextMenuElem, false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
handled = false;
|
handled = false;
|
||||||
|
@ -241,9 +258,8 @@ export class ContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderMenu(hasBackground=this.props.hasBackground) {
|
protected renderMenu(hasBackground = this.props.hasBackground) {
|
||||||
const position = {};
|
const position: Partial<Writeable<DOMRect>> = {};
|
||||||
let chevronFace = null;
|
|
||||||
const props = this.props;
|
const props = this.props;
|
||||||
|
|
||||||
if (props.top) {
|
if (props.top) {
|
||||||
|
@ -252,23 +268,24 @@ export class ContextMenu extends React.Component {
|
||||||
position.bottom = props.bottom;
|
position.bottom = props.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let chevronFace: ChevronFace;
|
||||||
if (props.left) {
|
if (props.left) {
|
||||||
position.left = props.left;
|
position.left = props.left;
|
||||||
chevronFace = 'left';
|
chevronFace = ChevronFace.Left;
|
||||||
} else {
|
} else {
|
||||||
position.right = props.right;
|
position.right = props.right;
|
||||||
chevronFace = 'right';
|
chevronFace = ChevronFace.Right;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
|
const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null;
|
||||||
|
|
||||||
const chevronOffset = {};
|
const chevronOffset: CSSProperties = {};
|
||||||
if (props.chevronFace) {
|
if (props.chevronFace) {
|
||||||
chevronFace = props.chevronFace;
|
chevronFace = props.chevronFace;
|
||||||
}
|
}
|
||||||
const hasChevron = chevronFace && chevronFace !== "none";
|
const hasChevron = chevronFace && chevronFace !== ChevronFace.None;
|
||||||
|
|
||||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
if (chevronFace === ChevronFace.Top || chevronFace === ChevronFace.Bottom) {
|
||||||
chevronOffset.left = props.chevronOffset;
|
chevronOffset.left = props.chevronOffset;
|
||||||
} else if (position.top !== undefined) {
|
} else if (position.top !== undefined) {
|
||||||
const target = position.top;
|
const target = position.top;
|
||||||
|
@ -298,13 +315,13 @@ export class ContextMenu extends React.Component {
|
||||||
'mx_ContextualMenu_right': !hasChevron && position.right,
|
'mx_ContextualMenu_right': !hasChevron && position.right,
|
||||||
'mx_ContextualMenu_top': !hasChevron && position.top,
|
'mx_ContextualMenu_top': !hasChevron && position.top,
|
||||||
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
'mx_ContextualMenu_bottom': !hasChevron && position.bottom,
|
||||||
'mx_ContextualMenu_withChevron_left': chevronFace === 'left',
|
'mx_ContextualMenu_withChevron_left': chevronFace === ChevronFace.Left,
|
||||||
'mx_ContextualMenu_withChevron_right': chevronFace === 'right',
|
'mx_ContextualMenu_withChevron_right': chevronFace === ChevronFace.Right,
|
||||||
'mx_ContextualMenu_withChevron_top': chevronFace === 'top',
|
'mx_ContextualMenu_withChevron_top': chevronFace === ChevronFace.Top,
|
||||||
'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom',
|
'mx_ContextualMenu_withChevron_bottom': chevronFace === ChevronFace.Bottom,
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuStyle = {};
|
const menuStyle: CSSProperties = {};
|
||||||
if (props.menuWidth) {
|
if (props.menuWidth) {
|
||||||
menuStyle.width = props.menuWidth;
|
menuStyle.width = props.menuWidth;
|
||||||
}
|
}
|
||||||
|
@ -335,13 +352,28 @@ export class ContextMenu extends React.Component {
|
||||||
let background;
|
let background;
|
||||||
if (hasBackground) {
|
if (hasBackground) {
|
||||||
background = (
|
background = (
|
||||||
<div className="mx_ContextualMenu_background" style={wrapperStyle} onClick={this._onFinished} onContextMenu={this.onContextMenu} />
|
<div
|
||||||
|
className="mx_ContextualMenu_background"
|
||||||
|
style={wrapperStyle}
|
||||||
|
onClick={this.onFinished}
|
||||||
|
onContextMenu={this.onContextMenu}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_ContextualMenu_wrapper" style={{...position, ...wrapperStyle}} onKeyDown={this._onKeyDown} onContextMenu={this.onContextMenuPreventBubbling}>
|
<div
|
||||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect} role={this.props.managed ? "menu" : undefined}>
|
className="mx_ContextualMenu_wrapper"
|
||||||
|
style={{...position, ...wrapperStyle}}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onContextMenu={this.onContextMenuPreventBubbling}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={menuClasses}
|
||||||
|
style={menuStyle}
|
||||||
|
ref={this.collectContextMenuRect}
|
||||||
|
role={this.props.managed ? "menu" : undefined}
|
||||||
|
>
|
||||||
{ chevron }
|
{ chevron }
|
||||||
{ props.children }
|
{ props.children }
|
||||||
</div>
|
</div>
|
||||||
|
@ -350,195 +382,13 @@ export class ContextMenu extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.ReactChild {
|
||||||
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
|
return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Semantic component for representing the AccessibleButton which launches a <ContextMenu />
|
|
||||||
export const ContextMenuButton = ({ label, isExpanded, children, onClick, onContextMenu, ...props }) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton
|
|
||||||
{...props}
|
|
||||||
onClick={onClick}
|
|
||||||
onContextMenu={onContextMenu || onClick}
|
|
||||||
title={label}
|
|
||||||
aria-label={label}
|
|
||||||
aria-haspopup={true}
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ContextMenuButton.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string,
|
|
||||||
isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitem
|
|
||||||
export const MenuItem = ({children, label, ...props}) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
MenuItem.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=group for grouping menu radios/checkboxes
|
|
||||||
export const MenuGroup = ({children, label, ...props}) => {
|
|
||||||
return <div {...props} role="group" aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
MenuGroup.propTypes = {
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitemcheckbox
|
|
||||||
export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} role="menuitemcheckbox" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
MenuItemCheckbox.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
active: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a styled role=menuitemcheckbox
|
|
||||||
export const StyledMenuItemCheckbox = ({children, label, onChange, onClose, checked, disabled=false, ...props}) => {
|
|
||||||
const onKeyDown = (e) => {
|
|
||||||
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
onChange();
|
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
|
||||||
if (e.key === Key.ENTER) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onKeyUp = (e) => {
|
|
||||||
// prevent the input default handler as we handle it on keydown to match
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
|
||||||
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<StyledCheckbox
|
|
||||||
{...props}
|
|
||||||
role="menuitemcheckbox"
|
|
||||||
aria-checked={checked}
|
|
||||||
checked={checked}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label={label}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onKeyUp={onKeyUp}
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</StyledCheckbox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
StyledMenuItemCheckbox.propTypes = {
|
|
||||||
...StyledCheckbox.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
checked: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a role=menuitemradio
|
|
||||||
export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => {
|
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
|
||||||
return (
|
|
||||||
<AccessibleButton {...props} role="menuitemradio" aria-checked={active} aria-disabled={disabled} tabIndex={-1} aria-label={label}>
|
|
||||||
{ children }
|
|
||||||
</AccessibleButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
MenuItemRadio.propTypes = {
|
|
||||||
...AccessibleButton.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
active: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Semantic component for representing a styled role=menuitemradio
|
|
||||||
export const StyledMenuItemRadio = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
|
|
||||||
const onKeyDown = (e) => {
|
|
||||||
if (e.key === Key.ENTER || e.key === Key.SPACE) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
onChange();
|
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
|
||||||
if (e.key === Key.ENTER) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const onKeyUp = (e) => {
|
|
||||||
// prevent the input default handler as we handle it on keydown to match
|
|
||||||
// https://www.w3.org/TR/wai-aria-practices/examples/menubar/menubar-2/menubar-2.html
|
|
||||||
if (e.key === Key.SPACE || e.key === Key.ENTER) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<StyledRadioButton
|
|
||||||
{...props}
|
|
||||||
role="menuitemradio"
|
|
||||||
aria-checked={checked}
|
|
||||||
checked={checked}
|
|
||||||
aria-disabled={disabled}
|
|
||||||
tabIndex={-1}
|
|
||||||
aria-label={label}
|
|
||||||
onChange={onChange}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onKeyUp={onKeyUp}
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</StyledRadioButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
StyledMenuItemRadio.propTypes = {
|
|
||||||
...StyledMenuItemRadio.propTypes,
|
|
||||||
label: PropTypes.string, // optional
|
|
||||||
checked: PropTypes.bool.isRequired,
|
|
||||||
disabled: PropTypes.bool, // optional
|
|
||||||
className: PropTypes.string, // optional
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
|
|
||||||
};
|
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
// Placement method for <ContextMenu /> to position context menu to right of elementRect with chevronOffset
|
||||||
export const toRightOf = (elementRect, chevronOffset=12) => {
|
export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
|
||||||
const left = elementRect.right + window.pageXOffset + 3;
|
const left = elementRect.right + window.pageXOffset + 3;
|
||||||
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
|
||||||
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
top -= chevronOffset + 8; // where 8 is half the height of the chevron
|
||||||
|
@ -546,8 +396,8 @@ export const toRightOf = (elementRect, chevronOffset=12) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
|
||||||
export const aboveLeftOf = (elementRect, chevronFace="none") => {
|
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => {
|
||||||
const menuOptions = { chevronFace };
|
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
|
||||||
|
|
||||||
const buttonRight = elementRect.right + window.pageXOffset;
|
const buttonRight = elementRect.right + window.pageXOffset;
|
||||||
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
const buttonBottom = elementRect.bottom + window.pageYOffset;
|
||||||
|
@ -605,3 +455,12 @@ export function createMenu(ElementClass, props) {
|
||||||
|
|
||||||
return {close: onFinished};
|
return {close: onFinished};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// re-export the semantic helper components for simplicity
|
||||||
|
export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
|
||||||
|
export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
|
||||||
|
export {MenuItem} from "../../accessibility/context_menu/MenuItem";
|
||||||
|
export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";
|
||||||
|
export {MenuItemRadio} from "../../accessibility/context_menu/MenuItemRadio";
|
||||||
|
export {StyledMenuItemCheckbox} from "../../accessibility/context_menu/StyledMenuItemCheckbox";
|
||||||
|
export {StyledMenuItemRadio} from "../../accessibility/context_menu/StyledMenuItemRadio";
|
|
@ -21,6 +21,7 @@ import classNames from "classnames";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import RoomList2 from "../views/rooms/RoomList2";
|
import RoomList2 from "../views/rooms/RoomList2";
|
||||||
|
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist2";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import UserMenu from "./UserMenu";
|
import UserMenu from "./UserMenu";
|
||||||
import RoomSearch from "./RoomSearch";
|
import RoomSearch from "./RoomSearch";
|
||||||
|
@ -114,64 +115,131 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleStickyHeaders(list: HTMLDivElement) {
|
private handleStickyHeaders(list: HTMLDivElement) {
|
||||||
// TODO: Evaluate if this has any performance benefit or detriment.
|
|
||||||
// See https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
|
|
||||||
if (this.isDoingStickyHeaders) return;
|
if (this.isDoingStickyHeaders) return;
|
||||||
this.isDoingStickyHeaders = true;
|
this.isDoingStickyHeaders = true;
|
||||||
if (window.requestAnimationFrame) {
|
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
this.doStickyHeaders(list);
|
this.doStickyHeaders(list);
|
||||||
this.isDoingStickyHeaders = false;
|
this.isDoingStickyHeaders = false;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
this.doStickyHeaders(list);
|
|
||||||
this.isDoingStickyHeaders = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private doStickyHeaders(list: HTMLDivElement) {
|
private doStickyHeaders(list: HTMLDivElement) {
|
||||||
const rlRect = list.getBoundingClientRect();
|
const topEdge = list.scrollTop;
|
||||||
const bottom = rlRect.bottom;
|
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||||
const top = rlRect.top;
|
|
||||||
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
||||||
const headerHeight = 32; // Note: must match the CSS!
|
|
||||||
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
|
|
||||||
|
|
||||||
const headerStickyWidth = rlRect.width - headerRightMargin;
|
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles
|
||||||
|
const headerStickyWidth = list.clientWidth - headerRightMargin;
|
||||||
|
|
||||||
|
// We track which styles we want on a target before making the changes to avoid
|
||||||
|
// excessive layout updates.
|
||||||
|
const targetStyles = new Map<HTMLDivElement, {
|
||||||
|
stickyTop?: boolean;
|
||||||
|
stickyBottom?: boolean;
|
||||||
|
makeInvisible?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
let gotBottom = false;
|
|
||||||
let lastTopHeader;
|
let lastTopHeader;
|
||||||
|
let firstBottomHeader;
|
||||||
for (const sublist of sublists) {
|
for (const sublist of sublists) {
|
||||||
const slRect = sublist.getBoundingClientRect();
|
|
||||||
|
|
||||||
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
|
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
|
||||||
|
header.style.removeProperty("display"); // always clear display:none first
|
||||||
|
|
||||||
if (slRect.top + headerHeight > bottom && !gotBottom) {
|
// When an element is <=40% off screen, make it take over
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
const offScreenFactor = 0.4;
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
const isOffTop = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) <= topEdge;
|
||||||
header.style.width = `${headerStickyWidth}px`;
|
const isOffBottom = (sublist.offsetTop + (offScreenFactor * HEADER_HEIGHT)) >= bottomEdge;
|
||||||
header.style.removeProperty("top");
|
|
||||||
gotBottom = true;
|
if (isOffTop || sublist === sublists[0]) {
|
||||||
} else if ((slRect.top - (headerHeight / 3)) < top) {
|
targetStyles.set(header, { stickyTop: true });
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
|
||||||
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
|
||||||
header.style.width = `${headerStickyWidth}px`;
|
|
||||||
header.style.top = `${rlRect.top}px`;
|
|
||||||
if (lastTopHeader) {
|
if (lastTopHeader) {
|
||||||
lastTopHeader.style.display = "none";
|
lastTopHeader.style.display = "none";
|
||||||
|
targetStyles.set(lastTopHeader, { makeInvisible: true });
|
||||||
}
|
}
|
||||||
// first unset it, if set in last iteration
|
|
||||||
header.style.removeProperty("display");
|
|
||||||
lastTopHeader = header;
|
lastTopHeader = header;
|
||||||
|
} else if (isOffBottom && !firstBottomHeader) {
|
||||||
|
targetStyles.set(header, { stickyBottom: true });
|
||||||
|
firstBottomHeader = header;
|
||||||
} else {
|
} else {
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
targetStyles.set(header, {}); // nothing == clear
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
|
||||||
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
|
||||||
header.style.removeProperty("width");
|
|
||||||
header.style.removeProperty("top");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run over the style changes and make them reality. We check to see if we're about to
|
||||||
|
// cause a no-op update, as adding/removing properties that are/aren't there cause
|
||||||
|
// layout updates.
|
||||||
|
for (const header of targetStyles.keys()) {
|
||||||
|
const style = targetStyles.get(header);
|
||||||
|
const headerContainer = header.parentElement; // .mx_RoomSublist2_headerContainer
|
||||||
|
|
||||||
|
if (style.makeInvisible) {
|
||||||
|
// we will have already removed the 'display: none', so add it back.
|
||||||
|
header.style.display = "none";
|
||||||
|
continue; // nothing else to do, even if sticky somehow
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.stickyTop) {
|
||||||
|
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTop = `${list.parentElement.offsetTop}px`;
|
||||||
|
if (header.style.top !== newTop) {
|
||||||
|
header.style.top = newTop;
|
||||||
|
}
|
||||||
|
} else if (style.stickyBottom) {
|
||||||
|
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.stickyTop || style.stickyBottom) {
|
||||||
|
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
|
||||||
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
}
|
||||||
|
if (!headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
|
||||||
|
headerContainer.classList.add("mx_RoomSublist2_headerContainer_hasSticky");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = `${headerStickyWidth}px`;
|
||||||
|
if (header.style.width !== newWidth) {
|
||||||
|
header.style.width = newWidth;
|
||||||
|
}
|
||||||
|
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||||
|
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
||||||
|
}
|
||||||
|
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
||||||
|
}
|
||||||
|
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
|
||||||
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
||||||
|
}
|
||||||
|
if (headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
|
||||||
|
headerContainer.classList.remove("mx_RoomSublist2_headerContainer_hasSticky");
|
||||||
|
}
|
||||||
|
if (header.style.width) {
|
||||||
|
header.style.removeProperty('width');
|
||||||
|
}
|
||||||
|
if (header.style.top) {
|
||||||
|
header.style.removeProperty('top');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add appropriate sticky classes to wrapper so it has
|
||||||
|
// the necessary top/bottom padding to put the sticky header in
|
||||||
|
const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper
|
||||||
|
if (lastTopHeader) {
|
||||||
|
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop");
|
||||||
|
} else {
|
||||||
|
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop");
|
||||||
|
}
|
||||||
|
if (firstBottomHeader) {
|
||||||
|
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom");
|
||||||
|
} else {
|
||||||
|
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Improve header reliability: https://github.com/vector-im/riot-web/issues/14232
|
// TODO: Improve header reliability: https://github.com/vector-im/riot-web/issues/14232
|
||||||
|
@ -325,6 +393,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
<aside className="mx_LeftPanel2_roomListContainer">
|
<aside className="mx_LeftPanel2_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchExplore()}
|
||||||
|
<div className="mx_LeftPanel2_roomListWrapper">
|
||||||
<div
|
<div
|
||||||
className={roomListClasses}
|
className={roomListClasses}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
|
@ -335,6 +404,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
>
|
>
|
||||||
{roomList}
|
{roomList}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -52,6 +52,7 @@ import {
|
||||||
} from "../../toasts/ServerLimitToast";
|
} from "../../toasts/ServerLimitToast";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import LeftPanel2 from "./LeftPanel2";
|
import LeftPanel2 from "./LeftPanel2";
|
||||||
|
import CallContainer from '../views/voip/CallContainer';
|
||||||
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||||
|
|
||||||
// We need to fetch each pinned message individually (if we don't already have it)
|
// We need to fetch each pinned message individually (if we don't already have it)
|
||||||
|
@ -696,6 +697,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
<CallContainer />
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
private openSearch = () => {
|
private openSearch = () => {
|
||||||
defaultDispatcher.dispatch({action: "show_left_panel"});
|
defaultDispatcher.dispatch({action: "show_left_panel"});
|
||||||
|
defaultDispatcher.dispatch({action: "focus_room_filter"});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange = () => {
|
private onChange = () => {
|
||||||
|
|
|
@ -2044,6 +2044,7 @@ export default createReactClass({
|
||||||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||||
jumpToBottom = (<JumpToBottomButton
|
jumpToBottom = (<JumpToBottomButton
|
||||||
|
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
/>);
|
/>);
|
||||||
|
|
|
@ -14,14 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import React, { createRef } from "react";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { createRef } from "react";
|
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import {ContextMenu, ContextMenuButton, MenuItem} from "./ContextMenu";
|
import { ChevronFace, ContextMenu, ContextMenuButton, MenuItem } from "./ContextMenu";
|
||||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||||
|
@ -122,7 +121,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: InputEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -235,7 +234,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronFace="none"
|
chevronFace={ChevronFace.None}
|
||||||
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
||||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
||||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||||
|
@ -281,11 +280,11 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
label={_t("All settings")}
|
label={_t("All settings")}
|
||||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||||
/>
|
/>
|
||||||
<MenuButton
|
{/* <MenuButton
|
||||||
iconClassName="mx_UserMenu_iconArchive"
|
iconClassName="mx_UserMenu_iconArchive"
|
||||||
label={_t("Archived rooms")}
|
label={_t("Archived rooms")}
|
||||||
onClick={this.onShowArchived}
|
onClick={this.onShowArchived}
|
||||||
/>
|
/> */}
|
||||||
<MenuButton
|
<MenuButton
|
||||||
iconClassName="mx_UserMenu_iconMessage"
|
iconClassName="mx_UserMenu_iconMessage"
|
||||||
label={_t("Feedback")}
|
label={_t("Feedback")}
|
||||||
|
@ -348,8 +347,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
{name}
|
{name}
|
||||||
{buttons}
|
{buttons}
|
||||||
</div>
|
</div>
|
||||||
{this.renderContextMenu()}
|
|
||||||
</ContextMenuButton>
|
</ContextMenuButton>
|
||||||
|
{this.renderContextMenu()}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import * as AvatarLogic from '../../../Avatar';
|
import * as AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
@ -26,9 +26,25 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import {toPx} from "../../../utils/units";
|
import {toPx} from "../../../utils/units";
|
||||||
|
|
||||||
const useImageUrl = ({url, urls}) => {
|
interface IProps {
|
||||||
const [imageUrls, setUrls] = useState([]);
|
name: string; // The name (first initial used as default)
|
||||||
const [urlsIndex, setIndex] = useState();
|
idName?: string; // ID for generating hash colours
|
||||||
|
title?: string; // onHover title text
|
||||||
|
url?: string; // highest priority of them all, shortcut to set in urls[0]
|
||||||
|
urls?: string[]; // [highest_priority, ... , lowest_priority]
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
// XXX: resizeMethod not actually used.
|
||||||
|
resizeMethod?: string;
|
||||||
|
defaultToInitialLetter?: boolean; // true to add default url
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
inputRef?: React.RefObject<HTMLImageElement & HTMLSpanElement>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useImageUrl = ({url, urls}): [string, () => void] => {
|
||||||
|
const [imageUrls, setUrls] = useState<string[]>([]);
|
||||||
|
const [urlsIndex, setIndex] = useState<number>();
|
||||||
|
|
||||||
const onError = useCallback(() => {
|
const onError = useCallback(() => {
|
||||||
setIndex(i => i + 1); // try the next one
|
setIndex(i => i + 1); // try the next one
|
||||||
|
@ -70,7 +86,7 @@ const useImageUrl = ({url, urls}) => {
|
||||||
return [imageUrl, onError];
|
return [imageUrl, onError];
|
||||||
};
|
};
|
||||||
|
|
||||||
const BaseAvatar = (props) => {
|
const BaseAvatar = (props: IProps) => {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
idName,
|
idName,
|
||||||
|
@ -117,7 +133,7 @@ const BaseAvatar = (props) => {
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onClick != null) {
|
if (onClick !== null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
@ -132,7 +148,12 @@ const BaseAvatar = (props) => {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps} role="presentation">
|
<span
|
||||||
|
className="mx_BaseAvatar"
|
||||||
|
ref={inputRef}
|
||||||
|
{...otherProps}
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
{ textNode }
|
{ textNode }
|
||||||
{ imgNode }
|
{ imgNode }
|
||||||
</span>
|
</span>
|
||||||
|
@ -140,7 +161,7 @@ const BaseAvatar = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onClick != null) {
|
if (onClick !== null) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||||
|
@ -173,26 +194,5 @@ const BaseAvatar = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
BaseAvatar.displayName = "BaseAvatar";
|
|
||||||
|
|
||||||
BaseAvatar.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
|
||||||
idName: PropTypes.string, // ID for generating hash colours
|
|
||||||
title: PropTypes.string, // onHover title text
|
|
||||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
|
||||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
// XXX resizeMethod not actually used.
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
inputRef: PropTypes.oneOfType([
|
|
||||||
// Either a function
|
|
||||||
PropTypes.func,
|
|
||||||
// Or the instance of a DOM native element
|
|
||||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BaseAvatar;
|
export default BaseAvatar;
|
||||||
|
export type BaseAvatarType = React.FC<IProps>;
|
|
@ -21,8 +21,8 @@ import { TagID } from '../../../stores/room-list/models';
|
||||||
import RoomAvatar from "./RoomAvatar";
|
import RoomAvatar from "./RoomAvatar";
|
||||||
import RoomTileIcon from "../rooms/RoomTileIcon";
|
import RoomTileIcon from "../rooms/RoomTileIcon";
|
||||||
import NotificationBadge from '../rooms/NotificationBadge';
|
import NotificationBadge from '../rooms/NotificationBadge';
|
||||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -33,7 +33,7 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
notificationState?: INotificationState;
|
notificationState?: NotificationState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
||||||
|
@ -42,7 +42,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,43 +15,36 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import BaseAvatar from './BaseAvatar';
|
||||||
|
|
||||||
export default createReactClass({
|
export interface IProps {
|
||||||
displayName: 'GroupAvatar',
|
groupId?: string;
|
||||||
|
groupName?: string;
|
||||||
|
groupAvatarUrl?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
resizeMethod?: string;
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
propTypes: {
|
export default class GroupAvatar extends React.Component<IProps> {
|
||||||
groupId: PropTypes.string,
|
public static defaultProps = {
|
||||||
groupName: PropTypes.string,
|
|
||||||
groupAvatarUrl: PropTypes.string,
|
|
||||||
width: PropTypes.number,
|
|
||||||
height: PropTypes.number,
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
getGroupAvatarUrl: function() {
|
getGroupAvatarUrl() {
|
||||||
return MatrixClientPeg.get().mxcUrlToHttp(
|
return MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
this.props.groupAvatarUrl,
|
this.props.groupAvatarUrl,
|
||||||
this.props.width,
|
this.props.width,
|
||||||
this.props.height,
|
this.props.height,
|
||||||
this.props.resizeMethod,
|
this.props.resizeMethod,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
|
||||||
// extract the props we use from props so we can pass any others through
|
// extract the props we use from props so we can pass any others through
|
||||||
// should consider adding this as a global rule in js-sdk?
|
// should consider adding this as a global rule in js-sdk?
|
||||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||||
|
@ -65,5 +58,5 @@ export default createReactClass({
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -16,48 +16,50 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
import BaseAvatar from "./BaseAvatar";
|
||||||
|
|
||||||
export default createReactClass({
|
interface IProps {
|
||||||
displayName: 'MemberAvatar',
|
// TODO: replace with correct type
|
||||||
|
member: any;
|
||||||
propTypes: {
|
fallbackUserId: string;
|
||||||
member: PropTypes.object,
|
width: number;
|
||||||
fallbackUserId: PropTypes.string,
|
height: number;
|
||||||
width: PropTypes.number,
|
resizeMethod: string;
|
||||||
height: PropTypes.number,
|
|
||||||
resizeMethod: PropTypes.string,
|
|
||||||
// The onClick to give the avatar
|
// The onClick to give the avatar
|
||||||
onClick: PropTypes.func,
|
onClick: React.MouseEventHandler;
|
||||||
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
// Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser`
|
||||||
viewUserOnClick: PropTypes.bool,
|
viewUserOnClick: boolean;
|
||||||
title: PropTypes.string,
|
title: string;
|
||||||
},
|
}
|
||||||
|
|
||||||
getDefaultProps: function() {
|
interface IState {
|
||||||
return {
|
name: string;
|
||||||
|
title: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MemberAvatar extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps = {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
viewUserOnClick: false,
|
viewUserOnClick: false,
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
constructor(props: IProps) {
|
||||||
return this._getState(this.props);
|
super(props);
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
this.state = MemberAvatar.getState(props);
|
||||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
}
|
||||||
this.setState(this._getState(nextProps));
|
|
||||||
},
|
|
||||||
|
|
||||||
_getState: function(props) {
|
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||||
|
return MemberAvatar.getState(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getState(props: IProps): IState {
|
||||||
if (props.member && props.member.name) {
|
if (props.member && props.member.name) {
|
||||||
return {
|
return {
|
||||||
name: props.member.name,
|
name: props.member.name,
|
||||||
|
@ -79,11 +81,9 @@ export default createReactClass({
|
||||||
} else {
|
} else {
|
||||||
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
console.error("MemberAvatar called somehow with null member or fallbackUserId");
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
render: function() {
|
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
|
||||||
|
|
||||||
|
render() {
|
||||||
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
let {member, fallbackUserId, onClick, viewUserOnClick, ...otherProps} = this.props;
|
||||||
const userId = member ? member.userId : fallbackUserId;
|
const userId = member ? member.userId : fallbackUserId;
|
||||||
|
|
||||||
|
@ -100,5 +100,5 @@ export default createReactClass({
|
||||||
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
<BaseAvatar {...otherProps} name={this.state.name} title={this.state.title}
|
||||||
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
idName={userId} url={this.state.imageUrl} onClick={onClick} />
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
import React from 'react';
|
||||||
import { NotificationColor } from "./NotificationColor";
|
|
||||||
|
|
||||||
export const NOTIFICATION_STATE_UPDATE = "update";
|
interface IProps {
|
||||||
|
|
||||||
export interface INotificationState extends EventEmitter {
|
|
||||||
symbol?: string;
|
|
||||||
count: number;
|
|
||||||
color: NotificationColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PulsedAvatar: React.FC<IProps> = (props) => {
|
||||||
|
return <div className="mx_PulsedAvatar">
|
||||||
|
{props.children}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PulsedAvatar;
|
|
@ -13,90 +13,96 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
import createReactClass from 'create-react-class';
|
import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
|
import BaseAvatar from './BaseAvatar';
|
||||||
|
import ImageView from '../elements/ImageView';
|
||||||
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
|
||||||
|
|
||||||
export default createReactClass({
|
|
||||||
displayName: 'RoomAvatar',
|
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
propTypes: {
|
room?: Room;
|
||||||
room: PropTypes.object,
|
// TODO: type when js-sdk has types
|
||||||
oobData: PropTypes.object,
|
oobData?: any;
|
||||||
width: PropTypes.number,
|
width?: number;
|
||||||
height: PropTypes.number,
|
height?: number;
|
||||||
resizeMethod: PropTypes.string,
|
resizeMethod?: string;
|
||||||
viewAvatarOnClick: PropTypes.bool,
|
viewAvatarOnClick?: boolean;
|
||||||
},
|
}
|
||||||
|
|
||||||
getDefaultProps: function() {
|
interface IState {
|
||||||
return {
|
urls: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RoomAvatar extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps = {
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
resizeMethod: 'crop',
|
resizeMethod: 'crop',
|
||||||
oobData: {},
|
oobData: {},
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
constructor(props: IProps) {
|
||||||
return {
|
super(props);
|
||||||
urls: this.getImageUrls(this.props),
|
|
||||||
|
this.state = {
|
||||||
|
urls: RoomAvatar.getImageUrls(this.props),
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
componentDidMount: function() {
|
public componentDidMount() {
|
||||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
public componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
cli.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
public static getDerivedStateFromProps(nextProps: IProps): IState {
|
||||||
UNSAFE_componentWillReceiveProps: function(newProps) {
|
return {
|
||||||
this.setState({
|
urls: RoomAvatar.getImageUrls(nextProps),
|
||||||
urls: this.getImageUrls(newProps),
|
};
|
||||||
});
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onRoomStateEvents: function(ev) {
|
// TODO: type when js-sdk has types
|
||||||
|
private onRoomStateEvents = (ev: any) => {
|
||||||
if (!this.props.room ||
|
if (!this.props.room ||
|
||||||
ev.getRoomId() !== this.props.room.roomId ||
|
ev.getRoomId() !== this.props.room.roomId ||
|
||||||
ev.getType() !== 'm.room.avatar'
|
ev.getType() !== 'm.room.avatar'
|
||||||
) return;
|
) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
urls: this.getImageUrls(this.props),
|
urls: RoomAvatar.getImageUrls(this.props),
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
getImageUrls: function(props) {
|
private static getImageUrls(props: IProps): string[] {
|
||||||
return [
|
return [
|
||||||
getHttpUriForMxc(
|
getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
// Default props don't play nicely with getDerivedStateFromProps
|
||||||
|
//props.oobData !== undefined ? props.oobData.avatarUrl : {},
|
||||||
props.oobData.avatarUrl,
|
props.oobData.avatarUrl,
|
||||||
Math.floor(props.width * window.devicePixelRatio),
|
Math.floor(props.width * window.devicePixelRatio),
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
), // highest priority
|
), // highest priority
|
||||||
this.getRoomAvatarUrl(props),
|
RoomAvatar.getRoomAvatarUrl(props),
|
||||||
].filter(function(url) {
|
].filter(function(url) {
|
||||||
return (url != null && url != "");
|
return (url !== null && url !== "");
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
getRoomAvatarUrl: function(props) {
|
private static getRoomAvatarUrl(props: IProps): string {
|
||||||
if (!props.room) return null;
|
if (!props.room) return null;
|
||||||
|
|
||||||
return Avatar.avatarUrlForRoom(
|
return Avatar.avatarUrlForRoom(
|
||||||
|
@ -105,24 +111,21 @@ export default createReactClass({
|
||||||
Math.floor(props.height * window.devicePixelRatio),
|
Math.floor(props.height * window.devicePixelRatio),
|
||||||
props.resizeMethod,
|
props.resizeMethod,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
onRoomAvatarClick: function() {
|
private onRoomAvatarClick = () => {
|
||||||
const avatarUrl = this.props.room.getAvatarUrl(
|
const avatarUrl = this.props.room.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
null, null, null, false);
|
null, null, null, false);
|
||||||
const ImageView = sdk.getComponent("elements.ImageView");
|
|
||||||
const params = {
|
const params = {
|
||||||
src: avatarUrl,
|
src: avatarUrl,
|
||||||
name: this.props.room.name,
|
name: this.props.room.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
|
||||||
},
|
};
|
||||||
|
|
||||||
render: function() {
|
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
|
||||||
|
|
||||||
|
public render() {
|
||||||
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
/*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
|
||||||
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props;
|
||||||
|
|
||||||
|
@ -132,8 +135,8 @@ export default createReactClass({
|
||||||
<BaseAvatar {...otherProps} name={roomName}
|
<BaseAvatar {...otherProps} name={roomName}
|
||||||
idName={room ? room.roomId : null}
|
idName={room ? room.roomId : null}
|
||||||
urls={this.state.urls}
|
urls={this.state.urls}
|
||||||
onClick={this.props.viewAvatarOnClick ? this.onRoomAvatarClick : null}
|
onClick={this.props.viewAvatarOnClick && !this.state.urls[0] ? this.onRoomAvatarClick : null}
|
||||||
disabled={!this.state.urls[0]} />
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
});
|
}
|
|
@ -64,7 +64,6 @@ export default function AccessibleButton({
|
||||||
className,
|
className,
|
||||||
...restProps
|
...restProps
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
|
|
||||||
const newProps: IAccessibleButtonProps = restProps;
|
const newProps: IAccessibleButtonProps = restProps;
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
newProps.onClick = onClick;
|
newProps.onClick = onClick;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {MatrixEvent} from "matrix-js-sdk";
|
import {MatrixEvent} from "matrix-js-sdk";
|
||||||
|
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'MemberEventListSummary',
|
displayName: 'MemberEventListSummary',
|
||||||
|
@ -284,6 +285,9 @@ export default createReactClass({
|
||||||
_getTransition: function(e) {
|
_getTransition: function(e) {
|
||||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||||
// Handle 3pid invites the same as invites so they get bundled together
|
// Handle 3pid invites the same as invites so they get bundled together
|
||||||
|
if (!isValid3pidInvite(e.mxEvent)) {
|
||||||
|
return 'invite_withdrawal';
|
||||||
|
}
|
||||||
return 'invited';
|
return 'invited';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,18 @@ limitations under the License.
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
|
const className = classNames({
|
||||||
|
'mx_JumpToBottomButton': true,
|
||||||
|
'mx_JumpToBottomButton_highlight': props.highlight,
|
||||||
|
});
|
||||||
let badge;
|
let badge;
|
||||||
if (props.numUnreadMessages) {
|
if (props.numUnreadMessages) {
|
||||||
badge = (<div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>);
|
badge = (<div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>);
|
||||||
}
|
}
|
||||||
return (<div className="mx_JumpToBottomButton">
|
return (<div className={className}>
|
||||||
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
|
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
|
||||||
title={_t("Scroll to most recent messages")}
|
title={_t("Scroll to most recent messages")}
|
||||||
onClick={props.onScrollToBottomClick}>
|
onClick={props.onScrollToBottomClick}>
|
||||||
|
|
|
@ -22,11 +22,10 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import { XOR } from "../../../@types/common";
|
import { XOR } from "../../../@types/common";
|
||||||
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState";
|
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
notification: INotificationState;
|
notification: NotificationState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, the badge will show a count if at all possible. This is typically
|
* If true, the badge will show a count if at all possible. This is typically
|
||||||
|
@ -97,19 +96,17 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
const {notification, forceCount, roomId, onClick, ...props} = this.props;
|
const {notification, forceCount, roomId, onClick, ...props} = this.props;
|
||||||
|
|
||||||
// Don't show a badge if we don't need to
|
// Don't show a badge if we don't need to
|
||||||
if (notification.color <= NotificationColor.None) return null;
|
if (notification.isIdle) return null;
|
||||||
|
|
||||||
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
|
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
|
||||||
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
|
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
|
||||||
// See git diff for what that boolean state looks like.
|
// See git diff for what that boolean state looks like.
|
||||||
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
|
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
|
||||||
const hasNotif = notification.color >= NotificationColor.Red;
|
|
||||||
const hasCount = notification.color >= NotificationColor.Grey;
|
|
||||||
const hasAnySymbol = notification.symbol || notification.count > 0;
|
const hasAnySymbol = notification.symbol || notification.count > 0;
|
||||||
let isEmptyBadge = !hasAnySymbol || !hasCount;
|
let isEmptyBadge = !hasAnySymbol || !notification.hasUnreadCount;
|
||||||
if (forceCount) {
|
if (forceCount) {
|
||||||
isEmptyBadge = false;
|
isEmptyBadge = false;
|
||||||
if (!hasCount) return null; // Can't render a badge
|
if (!notification.hasUnreadCount) return null; // Can't render a badge
|
||||||
}
|
}
|
||||||
|
|
||||||
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
|
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
|
||||||
|
@ -117,8 +114,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'mx_NotificationBadge': true,
|
'mx_NotificationBadge': true,
|
||||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasCount,
|
'mx_NotificationBadge_visible': isEmptyBadge ? true : notification.hasUnreadCount,
|
||||||
'mx_NotificationBadge_highlighted': hasNotif,
|
'mx_NotificationBadge_highlighted': notification.hasMentions,
|
||||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||||
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3,
|
||||||
'mx_NotificationBadge_3char': symbol.length > 2,
|
'mx_NotificationBadge_3char': symbol.length > 2,
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
@ -92,9 +91,6 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
// TODO: Decorate crumbs with icons: https://github.com/vector-im/riot-web/issues/14040
|
|
||||||
// TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
|
|
||||||
// TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
|
|
||||||
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
||||||
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
||||||
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
||||||
|
|
|
@ -32,15 +32,14 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import RoomSublist2 from "./RoomSublist2";
|
import RoomSublist2 from "./RoomSublist2";
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import GroupAvatar from "../avatars/GroupAvatar";
|
import GroupAvatar from "../avatars/GroupAvatar";
|
||||||
import TemporaryTile from "./TemporaryTile";
|
import TemporaryTile from "./TemporaryTile";
|
||||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
|
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||||
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -66,7 +65,6 @@ interface IProps {
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
sublists: ITagMap;
|
sublists: ITagMap;
|
||||||
layouts: Map<TagID, ListLayout>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAG_ORDER: TagID[] = [
|
const TAG_ORDER: TagID[] = [
|
||||||
|
@ -151,7 +149,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
sublists: {},
|
sublists: {},
|
||||||
layouts: new Map<TagID, ListLayout>(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
|
@ -215,14 +212,11 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
let listRooms = lists[t];
|
let listRooms = lists[t];
|
||||||
|
|
||||||
if (unread) {
|
if (unread) {
|
||||||
// TODO Be smarter and not spin up a bunch of wasted listeners just to kill them 4 lines later
|
|
||||||
// https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
const notificationStates = rooms.map(r => new TagSpecificNotificationState(r, t));
|
|
||||||
// filter to only notification rooms (and our current active room so we can index properly)
|
// filter to only notification rooms (and our current active room so we can index properly)
|
||||||
listRooms = notificationStates.filter(state => {
|
listRooms = listRooms.filter(r => {
|
||||||
return state.room.roomId === roomId || state.color >= NotificationColor.Bold;
|
const state = RoomNotificationStateStore.instance.getRoomState(r, t);
|
||||||
|
return state.room.roomId === roomId || state.isUnread;
|
||||||
});
|
});
|
||||||
notificationStates.forEach(state => state.destroy());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rooms.push(...listRooms);
|
rooms.push(...listRooms);
|
||||||
|
@ -238,12 +232,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
const newLists = RoomListStore.instance.orderedLists;
|
const newLists = RoomListStore.instance.orderedLists;
|
||||||
console.log("new lists", newLists);
|
console.log("new lists", newLists);
|
||||||
|
|
||||||
const layoutMap = new Map<TagID, ListLayout>();
|
this.setState({sublists: newLists}, () => {
|
||||||
for (const tagId of Object.keys(newLists)) {
|
|
||||||
layoutMap.set(tagId, new ListLayout(tagId));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({sublists: newLists, layouts: layoutMap}, () => {
|
|
||||||
this.props.onResize();
|
this.props.onResize();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -312,8 +301,6 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
label={_t(aesthetics.sectionLabel)}
|
label={_t(aesthetics.sectionLabel)}
|
||||||
onAddRoom={onAddRoomFn}
|
onAddRoom={onAddRoomFn}
|
||||||
addRoomLabel={aesthetics.addRoomLabel}
|
addRoomLabel={aesthetics.addRoomLabel}
|
||||||
isInvite={aesthetics.isInvite}
|
|
||||||
layout={this.state.layouts.get(orderedTagId)}
|
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
extraBadTilesThatShouldntExist={extraTiles}
|
extraBadTilesThatShouldntExist={extraTiles}
|
||||||
|
|
|
@ -24,9 +24,9 @@ import {RovingAccessibleButton, RovingTabIndexWrapper} from "../../../accessibil
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
import RoomTile2 from "./RoomTile2";
|
import RoomTile2 from "./RoomTile2";
|
||||||
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import {
|
import {
|
||||||
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuButton,
|
ContextMenuButton,
|
||||||
StyledMenuItemCheckbox,
|
StyledMenuItemCheckbox,
|
||||||
|
@ -40,7 +40,13 @@ import NotificationBadge from "./NotificationBadge";
|
||||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||||
|
import { Enable, Resizable } from "re-resizable";
|
||||||
|
import { Direction } from "re-resizable/lib/resizer";
|
||||||
|
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
||||||
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -55,9 +61,13 @@ import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
|
|
||||||
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
|
const SHOW_N_BUTTON_HEIGHT = 32; // As defined by CSS
|
||||||
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
||||||
|
export const HEADER_HEIGHT = 32; // As defined by CSS
|
||||||
|
|
||||||
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
|
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
|
||||||
|
|
||||||
|
// HACK: We really shouldn't have to do this.
|
||||||
|
polyfillTouchEvent();
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
forRooms: boolean;
|
forRooms: boolean;
|
||||||
rooms?: Room[];
|
rooms?: Room[];
|
||||||
|
@ -65,8 +75,6 @@ interface IProps {
|
||||||
label: string;
|
label: string;
|
||||||
onAddRoom?: () => void;
|
onAddRoom?: () => void;
|
||||||
addRoomLabel: string;
|
addRoomLabel: string;
|
||||||
isInvite: boolean;
|
|
||||||
layout?: ListLayout;
|
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
onResize: () => void;
|
onResize: () => void;
|
||||||
|
@ -89,16 +97,21 @@ interface IState {
|
||||||
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
private headerButton = createRef<HTMLDivElement>();
|
private headerButton = createRef<HTMLDivElement>();
|
||||||
private sublistRef = createRef<HTMLDivElement>();
|
private sublistRef = createRef<HTMLDivElement>();
|
||||||
|
private dispatcherRef: string;
|
||||||
|
private layout: ListLayout;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
notificationState: new ListNotificationState(this.props.isInvite, this.props.tagId),
|
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
};
|
};
|
||||||
this.state.notificationState.setRooms(this.props.rooms);
|
this.state.notificationState.setRooms(this.props.rooms);
|
||||||
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numTiles(): number {
|
private get numTiles(): number {
|
||||||
|
@ -106,8 +119,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numVisibleTiles(): number {
|
private get numVisibleTiles(): number {
|
||||||
if (!this.props.layout) return 0;
|
const nVisible = Math.floor(this.layout.visibleTiles);
|
||||||
const nVisible = Math.floor(this.props.layout.visibleTiles);
|
|
||||||
return Math.min(nVisible, this.numTiles);
|
return Math.min(nVisible, this.numTiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,17 +129,53 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.state.notificationState.destroy();
|
this.state.notificationState.destroy();
|
||||||
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) {
|
||||||
|
// XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
|
||||||
|
// where we lose the room we are changing from temporarily and then it comes back in an update right after.
|
||||||
|
setImmediate(() => {
|
||||||
|
const isCollapsed = this.layout.isCollapsed;
|
||||||
|
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
|
||||||
|
|
||||||
|
if (isCollapsed && roomIndex > -1) {
|
||||||
|
this.toggleCollapsed();
|
||||||
|
}
|
||||||
|
// extend the visible section to include the room if it is entirely invisible
|
||||||
|
if (roomIndex >= this.numVisibleTiles) {
|
||||||
|
this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
|
||||||
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onAddRoom = (e) => {
|
private onAddRoom = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (this.props.onAddRoom) this.props.onAddRoom();
|
if (this.props.onAddRoom) this.props.onAddRoom();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
|
private onResize = (
|
||||||
const direction = e.movementY < 0 ? -1 : +1;
|
e: MouseEvent | TouchEvent,
|
||||||
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
|
travelDirection: Direction,
|
||||||
this.props.layout.setVisibleTilesWithin(tileDiff, this.numTiles);
|
refToElement: HTMLDivElement,
|
||||||
|
delta: { width: number, height: number }, // TODO: Use re-resizer's NumberSize when it is exposed as the type
|
||||||
|
) => {
|
||||||
|
// Do some sanity checks, but in reality we shouldn't need these.
|
||||||
|
if (travelDirection !== "bottom") return;
|
||||||
|
if (delta.height === 0) return; // something went wrong, so just ignore it.
|
||||||
|
|
||||||
|
// NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate
|
||||||
|
// for our purposes. The delta provided by the library is also a change *from when
|
||||||
|
// resizing started*, meaning it is fairly useless for us. This is why we just use
|
||||||
|
// the client height and run with it.
|
||||||
|
|
||||||
|
const heightBefore = this.layout.visibleTiles;
|
||||||
|
const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight);
|
||||||
|
this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
|
||||||
|
if (heightBefore === this.layout.visibleTiles) return; // no-op
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,13 +189,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onShowAllClick = () => {
|
private onShowAllClick = () => {
|
||||||
const numVisibleTiles = this.numVisibleTiles;
|
const numVisibleTiles = this.numVisibleTiles;
|
||||||
this.props.layout.visibleTiles = this.props.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one
|
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowLessClick = () => {
|
private onShowLessClick = () => {
|
||||||
this.props.layout.visibleTiles = this.props.layout.defaultVisibleTiles;
|
this.layout.visibleTiles = this.layout.defaultVisibleTiles;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
// focus will flow to the show more button here
|
// focus will flow to the show more button here
|
||||||
};
|
};
|
||||||
|
@ -161,7 +209,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOpenMenuClick = (ev: InputEvent) => {
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -195,7 +243,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onMessagePreviewChanged = () => {
|
private onMessagePreviewChanged = () => {
|
||||||
this.props.layout.showPreviews = !this.props.layout.showPreviews;
|
this.layout.showPreviews = !this.layout.showPreviews;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -233,7 +281,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const possibleSticky = target.parentElement;
|
const possibleSticky = target.parentElement;
|
||||||
const sublist = possibleSticky.parentElement.parentElement;
|
const sublist = possibleSticky.parentElement.parentElement;
|
||||||
if (possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky')) {
|
const list = sublist.parentElement.parentElement;
|
||||||
|
// the scrollTop is capped at the height of the header in LeftPanel2
|
||||||
|
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
|
||||||
|
const isSticky = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky');
|
||||||
|
if (isSticky && !isAtTop) {
|
||||||
// is sticky - jump to list
|
// is sticky - jump to list
|
||||||
sublist.scrollIntoView({behavior: 'smooth'});
|
sublist.scrollIntoView({behavior: 'smooth'});
|
||||||
} else {
|
} else {
|
||||||
|
@ -243,13 +295,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private toggleCollapsed = () => {
|
private toggleCollapsed = () => {
|
||||||
this.props.layout.isCollapsed = !this.props.layout.isCollapsed;
|
this.layout.isCollapsed = !this.layout.isCollapsed;
|
||||||
this.forceUpdate(); // because the layout doesn't trigger an update
|
this.forceUpdate(); // because the layout doesn't trigger an update
|
||||||
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
||||||
};
|
};
|
||||||
|
|
||||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
const isCollapsed = this.props.layout && this.props.layout.isCollapsed;
|
const isCollapsed = this.layout && this.layout.isCollapsed;
|
||||||
switch (ev.key) {
|
switch (ev.key) {
|
||||||
case Key.ARROW_LEFT:
|
case Key.ARROW_LEFT:
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -289,7 +341,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderVisibleTiles(): React.ReactElement[] {
|
private renderVisibleTiles(): React.ReactElement[] {
|
||||||
if (this.props.layout && this.props.layout.isCollapsed) {
|
if (this.layout && this.layout.isCollapsed) {
|
||||||
// don't waste time on rendering
|
// don't waste time on rendering
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -303,7 +355,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
<RoomTile2
|
<RoomTile2
|
||||||
room={room}
|
room={room}
|
||||||
key={`room-${room.roomId}`}
|
key={`room-${room.roomId}`}
|
||||||
showMessagePreview={this.props.layout.showPreviews}
|
showMessagePreview={this.layout.showPreviews}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
tag={this.props.tagId}
|
tag={this.props.tagId}
|
||||||
/>
|
/>
|
||||||
|
@ -354,7 +406,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
<StyledMenuItemCheckbox
|
<StyledMenuItemCheckbox
|
||||||
onClose={this.onCloseMenu}
|
onClose={this.onCloseMenu}
|
||||||
onChange={this.onMessagePreviewChanged}
|
onChange={this.onMessagePreviewChanged}
|
||||||
checked={this.props.layout.showPreviews}
|
checked={this.layout.showPreviews}
|
||||||
>
|
>
|
||||||
{_t("Message preview")}
|
{_t("Message preview")}
|
||||||
</StyledMenuItemCheckbox>
|
</StyledMenuItemCheckbox>
|
||||||
|
@ -365,7 +417,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
chevronFace="none"
|
chevronFace={ChevronFace.None}
|
||||||
left={this.state.contextMenuPosition.left}
|
left={this.state.contextMenuPosition.left}
|
||||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||||
onFinished={this.onCloseMenu}
|
onFinished={this.onCloseMenu}
|
||||||
|
@ -446,7 +498,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const collapseClasses = classNames({
|
const collapseClasses = classNames({
|
||||||
'mx_RoomSublist2_collapseBtn': true,
|
'mx_RoomSublist2_collapseBtn': true,
|
||||||
'mx_RoomSublist2_collapseBtn_collapsed': this.props.layout && this.props.layout.isCollapsed,
|
'mx_RoomSublist2_collapseBtn_collapsed': this.layout && this.layout.isCollapsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -474,7 +526,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className="mx_RoomSublist2_headerText"
|
className="mx_RoomSublist2_headerText"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
aria-expanded={!this.props.layout || !this.props.layout.isCollapsed}
|
aria-expanded={!this.layout.isCollapsed}
|
||||||
aria-level={1}
|
aria-level={1}
|
||||||
onClick={this.onHeaderClick}
|
onClick={this.onHeaderClick}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
|
@ -508,12 +560,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
if (visibleTiles.length > 0) {
|
if (visibleTiles.length > 0) {
|
||||||
const layout = this.props.layout; // to shorten calls
|
const layout = this.layout; // to shorten calls
|
||||||
|
|
||||||
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
|
|
||||||
const showMoreBtnClasses = classNames({
|
const showMoreBtnClasses = classNames({
|
||||||
'mx_RoomSublist2_showNButton': true,
|
'mx_RoomSublist2_showNButton': true,
|
||||||
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// If we're hiding rooms, show a 'show more' button to the user. This button
|
// If we're hiding rooms, show a 'show more' button to the user. This button
|
||||||
|
@ -537,7 +587,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
{showMoreText}
|
{showMoreText}
|
||||||
</RovingAccessibleButton>
|
</RovingAccessibleButton>
|
||||||
);
|
);
|
||||||
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.layout.defaultVisibleTiles) {
|
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) {
|
||||||
// we have all tiles visible - add a button to show less
|
// we have all tiles visible - add a button to show less
|
||||||
let showLessText = (
|
let showLessText = (
|
||||||
<span className='mx_RoomSublist2_showNButtonText'>
|
<span className='mx_RoomSublist2_showNButtonText'>
|
||||||
|
@ -556,9 +606,19 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Figure out if we need a handle
|
// Figure out if we need a handle
|
||||||
let handles = ['s'];
|
const handles: Enable = {
|
||||||
|
bottom: true, // the only one we need, but the others must be explicitly false
|
||||||
|
bottomLeft: false,
|
||||||
|
bottomRight: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
top: false,
|
||||||
|
topLeft: false,
|
||||||
|
topRight: false,
|
||||||
|
};
|
||||||
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
|
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
|
||||||
handles = []; // no handles, we're at a minimum
|
// we're at a minimum, don't have a bottom handle
|
||||||
|
handles.bottom = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have to account for padding so we can accommodate a 'show more' button and
|
// We have to account for padding so we can accommodate a 'show more' button and
|
||||||
|
@ -582,22 +642,33 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
|
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
|
||||||
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
|
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
|
||||||
|
|
||||||
|
// Now that we know our padding constraints, let's find out if we need to chop off the
|
||||||
|
// last rendered visible tile so it doesn't collide with the 'show more' button
|
||||||
|
let visibleUnpaddedTiles = Math.round(layout.visibleTiles - layout.pixelsToTiles(padding));
|
||||||
|
if (visibleUnpaddedTiles === visibleTiles.length - 1) {
|
||||||
|
const placeholder = <div className="mx_RoomSublist2_placeholder" key='placeholder' />;
|
||||||
|
visibleTiles.splice(visibleUnpaddedTiles, 1, placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimensions = {
|
||||||
|
height: tilesPx,
|
||||||
|
};
|
||||||
content = (
|
content = (
|
||||||
<ResizableBox
|
<Resizable
|
||||||
width={-1}
|
size={dimensions as any}
|
||||||
height={tilesPx}
|
minHeight={minTilesPx}
|
||||||
axis="y"
|
maxHeight={maxTilesPx}
|
||||||
minConstraints={[-1, minTilesPx]}
|
|
||||||
maxConstraints={[-1, maxTilesPx]}
|
|
||||||
resizeHandles={handles}
|
|
||||||
onResize={this.onResize}
|
|
||||||
className="mx_RoomSublist2_resizeBox"
|
|
||||||
onResizeStart={this.onResizeStart}
|
onResizeStart={this.onResizeStart}
|
||||||
onResizeStop={this.onResizeStop}
|
onResizeStop={this.onResizeStop}
|
||||||
|
onResize={this.onResize}
|
||||||
|
handleWrapperClass="mx_RoomSublist2_resizerHandles"
|
||||||
|
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
|
||||||
|
className="mx_RoomSublist2_resizeBox"
|
||||||
|
enable={handles}
|
||||||
>
|
>
|
||||||
{visibleTiles}
|
{visibleTiles}
|
||||||
{showNButton}
|
{showNButton}
|
||||||
</ResizableBox>
|
</Resizable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, {createRef} from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
|
@ -27,6 +27,7 @@ import { Key } from "../../../Keyboard";
|
||||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {
|
import {
|
||||||
|
ChevronFace,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuButton,
|
ContextMenuButton,
|
||||||
MenuItemRadio,
|
MenuItemRadio,
|
||||||
|
@ -45,11 +46,14 @@ import {
|
||||||
MUTE,
|
MUTE,
|
||||||
} from "../../../RoomNotifs";
|
} from "../../../RoomNotifs";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
|
||||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
|
||||||
import { Volume } from "../../../RoomNotifsTypes";
|
import { Volume } from "../../../RoomNotifsTypes";
|
||||||
|
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||||
|
import RoomListActions from "../../../actions/RoomListActions";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import {ActionPayload} from "../../../dispatcher/payloads";
|
||||||
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -75,7 +79,7 @@ type PartialDOMRect = Pick<DOMRect, "left" | "bottom">;
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
notificationState: INotificationState;
|
notificationState: NotificationState;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
notificationsMenuPosition: PartialDOMRect;
|
notificationsMenuPosition: PartialDOMRect;
|
||||||
generalMenuPosition: PartialDOMRect;
|
generalMenuPosition: PartialDOMRect;
|
||||||
|
@ -87,7 +91,7 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => {
|
||||||
// align the context menu's icons with the icon which opened the context menu
|
// align the context menu's icons with the icon which opened the context menu
|
||||||
const left = elementRect.left + window.pageXOffset - 9;
|
const left = elementRect.left + window.pageXOffset - 9;
|
||||||
const top = elementRect.bottom + window.pageYOffset + 17;
|
const top = elementRect.bottom + window.pageYOffset + 17;
|
||||||
const chevronFace = "none";
|
const chevronFace = ChevronFace.None;
|
||||||
return {left, top, chevronFace};
|
return {left, top, chevronFace};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -118,18 +122,23 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class RoomTile2 extends React.Component<IProps, IState> {
|
export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
private roomTileRef = createRef<HTMLDivElement>();
|
||||||
|
// TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hover: false,
|
hover: false,
|
||||||
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
||||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||||
notificationsMenuPosition: null,
|
notificationsMenuPosition: null,
|
||||||
generalMenuPosition: null,
|
generalMenuPosition: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get showContextMenu(): boolean {
|
private get showContextMenu(): boolean {
|
||||||
|
@ -140,12 +149,36 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
return !this.props.isMinimized && this.props.showMessagePreview;
|
return !this.props.isMinimized && this.props.showMessagePreview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
|
||||||
|
if (this.state.selected) {
|
||||||
|
this.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
}
|
}
|
||||||
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
if (payload.action === "view_room" && payload.room_id === this.props.room.roomId && payload.show_room_tile) {
|
||||||
|
setImmediate(() => {
|
||||||
|
this.scrollIntoView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private scrollIntoView = () => {
|
||||||
|
if (!this.roomTileRef.current) return;
|
||||||
|
this.roomTileRef.current.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
behavior: "auto",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onTileMouseEnter = () => {
|
private onTileMouseEnter = () => {
|
||||||
this.setState({hover: true});
|
this.setState({hover: true});
|
||||||
};
|
};
|
||||||
|
@ -159,7 +192,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
// TODO: Support show_room_tile in new room list: https://github.com/vector-im/riot-web/issues/14233
|
|
||||||
show_room_tile: true, // make sure the room is visible in the list
|
show_room_tile: true, // make sure the room is visible in the list
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
|
||||||
|
@ -170,7 +202,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({selected: isActive});
|
this.setState({selected: isActive});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNotificationsMenuOpenClick = (ev: InputEvent) => {
|
private onNotificationsMenuOpenClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -181,7 +213,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
this.setState({notificationsMenuPosition: null});
|
this.setState({notificationsMenuPosition: null});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGeneralMenuOpenClick = (ev: InputEvent) => {
|
private onGeneralMenuOpenClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const target = ev.target as HTMLButtonElement;
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
@ -210,8 +242,22 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
// TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
|
if (tagId === DefaultTagID.Favourite) {
|
||||||
// TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
|
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
||||||
|
const isFavourite = roomTags.includes(DefaultTagID.Favourite);
|
||||||
|
const removeTag = isFavourite ? DefaultTagID.Favourite : DefaultTagID.LowPriority;
|
||||||
|
const addTag = isFavourite ? null : DefaultTagID.Favourite;
|
||||||
|
dis.dispatch(RoomListActions.tagRoom(
|
||||||
|
MatrixClientPeg.get(),
|
||||||
|
this.props.room,
|
||||||
|
removeTag,
|
||||||
|
addTag,
|
||||||
|
undefined,
|
||||||
|
0
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
console.log(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
if ((ev as React.KeyboardEvent).key === Key.ENTER) {
|
||||||
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
|
||||||
|
@ -343,6 +389,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
|
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
|
||||||
|
|
||||||
|
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
||||||
|
|
||||||
|
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||||
|
const favouriteIconClassName = isFavorite ? "mx_RoomTile2_iconFavorite" : "mx_RoomTile2_iconStar";
|
||||||
|
const favouriteLabelClassName = isFavorite ? "mx_RoomTile2_contextMenu_activeRow" : "";
|
||||||
|
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (this.state.generalMenuPosition) {
|
if (this.state.generalMenuPosition) {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
|
@ -350,12 +403,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu">
|
||||||
<div className="mx_IconizedContextMenu_optionList">
|
<div className="mx_IconizedContextMenu_optionList">
|
||||||
<MenuItemCheckbox
|
<MenuItemCheckbox
|
||||||
|
className={favouriteLabelClassName}
|
||||||
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
||||||
active={false} // TODO: https://github.com/vector-im/riot-web/issues/14283
|
active={isFavorite}
|
||||||
label={_t("Favourite")}
|
label={favouriteLabel}
|
||||||
>
|
>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconStar" />
|
<span className={classNames("mx_IconizedContextMenu_icon", favouriteIconClassName)} />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Favourite")}</span>
|
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
|
||||||
</MenuItemCheckbox>
|
</MenuItemCheckbox>
|
||||||
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" />
|
||||||
|
@ -437,11 +491,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationColor = this.state.notificationState.color;
|
|
||||||
const nameClasses = classNames({
|
const nameClasses = classNames({
|
||||||
"mx_RoomTile2_name": true,
|
"mx_RoomTile2_name": true,
|
||||||
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
"mx_RoomTile2_nameWithPreview": !!messagePreview,
|
||||||
"mx_RoomTile2_nameHasUnreadEvents": notificationColor >= NotificationColor.Bold,
|
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.isUnread,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nameContainer = (
|
let nameContainer = (
|
||||||
|
@ -458,15 +511,15 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
|
||||||
if (this.props.tag === DefaultTagID.Invite) {
|
if (this.props.tag === DefaultTagID.Invite) {
|
||||||
// append nothing
|
// append nothing
|
||||||
} else if (notificationColor >= NotificationColor.Red) {
|
} else if (this.state.notificationState.hasMentions) {
|
||||||
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||||
count: this.state.notificationState.count,
|
count: this.state.notificationState.count,
|
||||||
});
|
});
|
||||||
} else if (notificationColor >= NotificationColor.Grey) {
|
} else if (this.state.notificationState.hasUnreadCount) {
|
||||||
ariaLabel += " " + _t("%(count)s unread messages.", {
|
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||||
count: this.state.notificationState.count,
|
count: this.state.notificationState.count,
|
||||||
});
|
});
|
||||||
} else if (notificationColor >= NotificationColor.Bold) {
|
} else if (this.state.notificationState.isUnread) {
|
||||||
ariaLabel += " " + _t("Unread messages.");
|
ariaLabel += " " + _t("Unread messages.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,7 +530,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
|
||||||
{({onFocus, isActive, ref}) =>
|
{({onFocus, isActive, ref}) =>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
|
|
|
@ -18,16 +18,15 @@ import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
avatar: React.ReactElement;
|
avatar: React.ReactElement;
|
||||||
notificationState: INotificationState;
|
notificationState: NotificationState;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +73,7 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const nameClasses = classNames({
|
const nameClasses = classNames({
|
||||||
"mx_RoomTile2_name": true,
|
"mx_RoomTile2_name": true,
|
||||||
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
|
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.isUnread,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nameContainer = (
|
let nameContainer = (
|
||||||
|
|
|
@ -22,6 +22,10 @@ import * as sdk from "../../../../..";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
|
import RoomListStore from "../../../../../stores/room-list/RoomListStore2";
|
||||||
|
import RoomListActions from "../../../../../actions/RoomListActions";
|
||||||
|
import { DefaultTagID } from '../../../../../stores/room-list/models';
|
||||||
|
import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch';
|
||||||
|
|
||||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -29,12 +33,16 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
closeSettingsFn: PropTypes.func.isRequired,
|
closeSettingsFn: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
|
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
||||||
|
const roomTags = RoomListStore.instance.getTagsForRoom(room);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// This is eventually set to the value of room.getRecommendedVersion()
|
// This is eventually set to the value of room.getRecommendedVersion()
|
||||||
upgradeRecommendation: null,
|
upgradeRecommendation: null,
|
||||||
|
isLowPriorityRoom: roomTags.includes(DefaultTagID.LowPriority),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +94,25 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
this.props.closeSettingsFn();
|
this.props.closeSettingsFn();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_onToggleLowPriorityTag = (e) => {
|
||||||
|
this.setState({
|
||||||
|
isLowPriorityRoom: !this.state.isLowPriorityRoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeTag = this.state.isLowPriorityRoom ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
||||||
|
const addTag = this.state.isLowPriorityRoom ? null : DefaultTagID.LowPriority;
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
dis.dispatch(RoomListActions.tagRoom(
|
||||||
|
client,
|
||||||
|
client.getRoom(this.props.roomId),
|
||||||
|
removeTag,
|
||||||
|
addTag,
|
||||||
|
undefined,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
|
@ -156,6 +183,17 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
{_t("Open Devtools")}
|
{_t("Open Devtools")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||||
|
<span className='mx_SettingsTab_subheading'>{_t('Make this room low priority')}</span>
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={this.state.isLowPriorityRoom}
|
||||||
|
onChange={this._onToggleLowPriorityTag}
|
||||||
|
label={_t(
|
||||||
|
"Low priority rooms show up at the bottom of your room list" +
|
||||||
|
" in a dedicated section at the bottom of your room list",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -402,6 +402,12 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
useCheckbox={true}
|
useCheckbox={true}
|
||||||
disabled={this.state.useIRCLayout}
|
disabled={this.state.useIRCLayout}
|
||||||
/>
|
/>
|
||||||
|
<SettingsFlag
|
||||||
|
name="useIRCLayout"
|
||||||
|
level={SettingLevel.DEVICE}
|
||||||
|
useCheckbox={true}
|
||||||
|
onChange={(checked) => this.setState({useIRCLayout: checked})}
|
||||||
|
/>
|
||||||
<SettingsFlag
|
<SettingsFlag
|
||||||
name="useSystemFont"
|
name="useSystemFont"
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
|
@ -440,7 +446,6 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
|
||||||
</div>
|
</div>
|
||||||
{this.renderThemeSection()}
|
{this.renderThemeSection()}
|
||||||
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
|
{SettingsStore.isFeatureEnabled("feature_font_scaling") ? this.renderFontSection() : null}
|
||||||
{SettingsStore.isFeatureEnabled("feature_irc_ui") ? this.renderLayoutSection() : null}
|
|
||||||
{this.renderAdvancedSection()}
|
{this.renderAdvancedSection()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
37
src/components/views/voip/CallContainer.tsx
Normal file
37
src/components/views/voip/CallContainer.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 IncomingCallBox2 from './IncomingCallBox2';
|
||||||
|
import CallPreview from './CallPreview2';
|
||||||
|
import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CallContainer extends React.PureComponent<IProps, IState> {
|
||||||
|
public render() {
|
||||||
|
return <div className="mx_CallContainer">
|
||||||
|
<IncomingCallBox2 />
|
||||||
|
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
129
src/components/views/voip/CallPreview2.tsx
Normal file
129
src/components/views/voip/CallPreview2.tsx
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
Copyright 2019, 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import CallView from "./CallView2";
|
||||||
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// A Conference Handler implementation
|
||||||
|
// Must have a function signature:
|
||||||
|
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||||
|
ConferenceHandler: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
roomId: string;
|
||||||
|
activeCall: any;
|
||||||
|
newRoomListActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
|
private roomStoreToken: any;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
private settingsWatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
roomId: RoomViewStore.getRoomId(),
|
||||||
|
activeCall: CallHandler.getAnyActiveCall(),
|
||||||
|
newRoomListActive: SettingsStore.getValue("feature_new_room_list"),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.settingsWatcherRef = SettingsStore.watchSetting("feature_new_room_list", null, (name, roomId, level, valAtLevel, newVal) => this.setState({
|
||||||
|
newRoomListActive: newVal,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
if (this.roomStoreToken) {
|
||||||
|
this.roomStoreToken.remove();
|
||||||
|
}
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRoomViewStoreUpdate = (payload) => {
|
||||||
|
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||||
|
this.setState({
|
||||||
|
roomId: RoomViewStore.getRoomId(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
switch (payload.action) {
|
||||||
|
// listen for call state changes to prod the render method, which
|
||||||
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
|
case 'call_state':
|
||||||
|
this.setState({
|
||||||
|
activeCall: CallHandler.getAnyActiveCall(),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onCallViewClick = () => {
|
||||||
|
const call = CallHandler.getAnyActiveCall();
|
||||||
|
if (call) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: call.groupRoomId || call.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.newRoomListActive) {
|
||||||
|
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
|
||||||
|
const showCall = (
|
||||||
|
this.state.activeCall &&
|
||||||
|
this.state.activeCall.call_state === 'connected' &&
|
||||||
|
!callForRoom
|
||||||
|
);
|
||||||
|
|
||||||
|
if (showCall) {
|
||||||
|
return (
|
||||||
|
<CallView
|
||||||
|
className="mx_CallPreview" onClick={this.onCallViewClick}
|
||||||
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
|
showHangup={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <PersistentApp />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
200
src/components/views/voip/CallView2.tsx
Normal file
200
src/components/views/voip/CallView2.tsx
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019, 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
import React, {createRef} from 'react';
|
||||||
|
import Room from 'matrix-js-sdk/src/models/room';
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import VideoView from "./VideoView";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// js-sdk room object. If set, we will only show calls for the given
|
||||||
|
// room; if not, we will show any active call.
|
||||||
|
room?: Room;
|
||||||
|
|
||||||
|
// A Conference Handler implementation
|
||||||
|
// Must have a function signature:
|
||||||
|
// getConferenceCallForRoom(roomId: string): MatrixCall
|
||||||
|
ConferenceHandler?: any;
|
||||||
|
|
||||||
|
// maxHeight style attribute for the video panel
|
||||||
|
maxVideoHeight?: number;
|
||||||
|
|
||||||
|
// a callback which is called when the user clicks on the video div
|
||||||
|
onClick?: React.MouseEventHandler;
|
||||||
|
|
||||||
|
// a callback which is called when the content in the callview changes
|
||||||
|
// in a way that is likely to cause a resize.
|
||||||
|
onResize?: any;
|
||||||
|
|
||||||
|
// classname applied to view,
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
// Whether to show the hang up icon:W
|
||||||
|
showHangup?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
call: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CallView extends React.Component<IProps, IState> {
|
||||||
|
private videoref: React.RefObject<any>;
|
||||||
|
private dispatcherRef: string;
|
||||||
|
public call: any;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// the call this view is displaying (if any)
|
||||||
|
call: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.videoref = createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this.showCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload) => {
|
||||||
|
// don't filter out payloads for room IDs other than props.room because
|
||||||
|
// we may be interested in the conf 1:1 room
|
||||||
|
if (payload.action !== 'call_state') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.showCall();
|
||||||
|
};
|
||||||
|
|
||||||
|
private showCall() {
|
||||||
|
let call;
|
||||||
|
|
||||||
|
if (this.props.room) {
|
||||||
|
const roomId = this.props.room.roomId;
|
||||||
|
call = CallHandler.getCallForRoom(roomId) ||
|
||||||
|
(this.props.ConferenceHandler ?
|
||||||
|
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.call) {
|
||||||
|
this.setState({ call: call });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call = CallHandler.getAnyActiveCall();
|
||||||
|
// Ignore calls if we can't get the room associated with them.
|
||||||
|
// I think the underlying problem is that the js-sdk sends events
|
||||||
|
// for calls before it has made the rooms available in the store,
|
||||||
|
// although this isn't confirmed.
|
||||||
|
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
|
||||||
|
call = null;
|
||||||
|
}
|
||||||
|
this.setState({ call: call });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call) {
|
||||||
|
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
|
||||||
|
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
|
||||||
|
// always use a separate element for audio stream playback.
|
||||||
|
// this is to let us move CallView around the DOM without interrupting remote audio
|
||||||
|
// during playback, by having the audio rendered by a top-level <audio/> element.
|
||||||
|
// rather than being rendered by the main remoteVideo <video/> element.
|
||||||
|
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
|
||||||
|
}
|
||||||
|
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
|
||||||
|
// if this call is a conf call, don't display local video as the
|
||||||
|
// conference will have us in it
|
||||||
|
this.getVideoView().getLocalVideoElement().style.display = (
|
||||||
|
call.confUserId ? "none" : "block"
|
||||||
|
);
|
||||||
|
this.getVideoView().getRemoteVideoElement().style.display = "block";
|
||||||
|
} else {
|
||||||
|
this.getVideoView().getLocalVideoElement().style.display = "none";
|
||||||
|
this.getVideoView().getRemoteVideoElement().style.display = "none";
|
||||||
|
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onResize) {
|
||||||
|
this.props.onResize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getVideoView() {
|
||||||
|
return this.videoref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let view: React.ReactNode;
|
||||||
|
if (this.state.call && this.state.call.type === "voice") {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const callRoom = client.getRoom(this.state.call.roomId);
|
||||||
|
|
||||||
|
view = <AccessibleButton className="mx_CallView2_voice" onClick={this.props.onClick}>
|
||||||
|
<PulsedAvatar>
|
||||||
|
<RoomAvatar
|
||||||
|
room={callRoom}
|
||||||
|
height={35}
|
||||||
|
width={35}
|
||||||
|
/>
|
||||||
|
</PulsedAvatar>
|
||||||
|
<div>
|
||||||
|
<h1>{callRoom.name}</h1>
|
||||||
|
<p>{ _t("Active call") }</p>
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>;
|
||||||
|
} else {
|
||||||
|
view = <VideoView
|
||||||
|
ref={this.videoref}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
onResize={this.props.onResize}
|
||||||
|
maxHeight={this.props.maxVideoHeight}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hangup: React.ReactNode;
|
||||||
|
if (this.props.showHangup) {
|
||||||
|
hangup = <div
|
||||||
|
className="mx_CallView2_hangup"
|
||||||
|
onClick={() => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
room_id: this.state.call.roomId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={this.props.className}>
|
||||||
|
{view}
|
||||||
|
{hangup}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
141
src/components/views/voip/IncomingCallBox2.tsx
Normal file
141
src/components/views/voip/IncomingCallBox2.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
Copyright 2019, 2020 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
|
import CallHandler from '../../../CallHandler';
|
||||||
|
import PulsedAvatar from '../avatars/PulsedAvatar';
|
||||||
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
|
import FormButton from '../elements/FormButton';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
incomingCall: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IncomingCallBox2 extends React.Component<IProps, IState> {
|
||||||
|
private dispatcherRef: string;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this.state = {
|
||||||
|
incomingCall: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onAction = (payload: ActionPayload) => {
|
||||||
|
switch (payload.action) {
|
||||||
|
case 'call_state':
|
||||||
|
const call = CallHandler.getCall(payload.room_id);
|
||||||
|
if (call && call.call_state === 'ringing') {
|
||||||
|
this.setState({
|
||||||
|
incomingCall: call,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
incomingCall: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onAnswerClick: React.MouseEventHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'answer',
|
||||||
|
room_id: this.state.incomingCall.roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRejectClick: React.MouseEventHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'hangup',
|
||||||
|
room_id: this.state.incomingCall.roomId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (!this.state.incomingCall) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let room = null;
|
||||||
|
if (this.state.incomingCall) {
|
||||||
|
room = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const caller = room ? room.name : _t("Unknown caller");
|
||||||
|
|
||||||
|
let incomingCallText = null;
|
||||||
|
if (this.state.incomingCall) {
|
||||||
|
if (this.state.incomingCall.type === "voice") {
|
||||||
|
incomingCallText = _t("Incoming voice call");
|
||||||
|
} else if (this.state.incomingCall.type === "video") {
|
||||||
|
incomingCallText = _t("Incoming video call");
|
||||||
|
} else {
|
||||||
|
incomingCallText = _t("Incoming call");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_IncomingCallBox2">
|
||||||
|
<div className="mx_IncomingCallBox2_CallerInfo">
|
||||||
|
<PulsedAvatar>
|
||||||
|
<RoomAvatar
|
||||||
|
room={room}
|
||||||
|
height={32}
|
||||||
|
width={32}
|
||||||
|
/>
|
||||||
|
</PulsedAvatar>
|
||||||
|
<div>
|
||||||
|
<h1>{caller}</h1>
|
||||||
|
<p>{incomingCallText}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_IncomingCallBox2_buttons">
|
||||||
|
<FormButton
|
||||||
|
className={"mx_IncomingCallBox2_decline"}
|
||||||
|
onClick={this.onRejectClick}
|
||||||
|
kind="danger"
|
||||||
|
label={_t("Decline")}
|
||||||
|
/>
|
||||||
|
<div className="mx_IncomingCallBox2_spacer" />
|
||||||
|
<FormButton
|
||||||
|
className={"mx_IncomingCallBox2_accept"}
|
||||||
|
onClick={this.onAnswerClick}
|
||||||
|
kind="primary"
|
||||||
|
label={_t("Accept")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -489,7 +489,6 @@
|
||||||
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
|
||||||
"Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)",
|
"Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)",
|
||||||
"Support adding custom themes": "Support adding custom themes",
|
"Support adding custom themes": "Support adding custom themes",
|
||||||
"Enable IRC layout option in the appearance tab": "Enable IRC layout option in the appearance tab",
|
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
|
@ -539,7 +538,7 @@
|
||||||
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
|
"How fast should messages be downloaded.": "How fast should messages be downloaded.",
|
||||||
"Manually verify all remote sessions": "Manually verify all remote sessions",
|
"Manually verify all remote sessions": "Manually verify all remote sessions",
|
||||||
"IRC display name width": "IRC display name width",
|
"IRC display name width": "IRC display name width",
|
||||||
"Use IRC layout": "Use IRC layout",
|
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
"Uploading report": "Uploading report",
|
"Uploading report": "Uploading report",
|
||||||
|
@ -558,12 +557,17 @@
|
||||||
"My Ban List": "My Ban List",
|
"My Ban List": "My Ban List",
|
||||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||||
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
|
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
|
||||||
|
"Active call": "Active call",
|
||||||
"unknown caller": "unknown caller",
|
"unknown caller": "unknown caller",
|
||||||
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
|
||||||
"Incoming video call from %(name)s": "Incoming video call from %(name)s",
|
"Incoming video call from %(name)s": "Incoming video call from %(name)s",
|
||||||
"Incoming call from %(name)s": "Incoming call from %(name)s",
|
"Incoming call from %(name)s": "Incoming call from %(name)s",
|
||||||
"Decline": "Decline",
|
"Decline": "Decline",
|
||||||
"Accept": "Accept",
|
"Accept": "Accept",
|
||||||
|
"Unknown caller": "Unknown caller",
|
||||||
|
"Incoming voice call": "Incoming voice call",
|
||||||
|
"Incoming video call": "Incoming video call",
|
||||||
|
"Incoming call": "Incoming call",
|
||||||
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
"The other party cancelled the verification.": "The other party cancelled the verification.",
|
||||||
"Verified!": "Verified!",
|
"Verified!": "Verified!",
|
||||||
"You've successfully verified this user.": "You've successfully verified this user.",
|
"You've successfully verified this user.": "You've successfully verified this user.",
|
||||||
|
@ -966,6 +970,8 @@
|
||||||
"Room version:": "Room version:",
|
"Room version:": "Room version:",
|
||||||
"Developer options": "Developer options",
|
"Developer options": "Developer options",
|
||||||
"Open Devtools": "Open Devtools",
|
"Open Devtools": "Open Devtools",
|
||||||
|
"Make this room low priority": "Make this room low priority",
|
||||||
|
"Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list",
|
||||||
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
|
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
|
||||||
"This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "This room isn’t bridging messages to any platforms. <a>Learn more.</a>",
|
"This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "This room isn’t bridging messages to any platforms. <a>Learn more.</a>",
|
||||||
"Bridges": "Bridges",
|
"Bridges": "Bridges",
|
||||||
|
@ -1224,6 +1230,7 @@
|
||||||
"All messages": "All messages",
|
"All messages": "All messages",
|
||||||
"Mentions & Keywords": "Mentions & Keywords",
|
"Mentions & Keywords": "Mentions & Keywords",
|
||||||
"Notification options": "Notification options",
|
"Notification options": "Notification options",
|
||||||
|
"Favourited": "Favourited",
|
||||||
"Favourite": "Favourite",
|
"Favourite": "Favourite",
|
||||||
"Leave Room": "Leave Room",
|
"Leave Room": "Leave Room",
|
||||||
"Room options": "Room options",
|
"Room options": "Room options",
|
||||||
|
@ -2102,7 +2109,6 @@
|
||||||
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
|
||||||
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
|
||||||
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
|
||||||
"Active call": "Active call",
|
|
||||||
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
"There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?": "There's no one else here! Would you like to <inviteText>invite others</inviteText> or <nowarnText>stop warning about the empty room</nowarnText>?",
|
||||||
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
|
||||||
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
"You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?",
|
||||||
|
@ -2130,7 +2136,6 @@
|
||||||
"Switch theme": "Switch theme",
|
"Switch theme": "Switch theme",
|
||||||
"Security & privacy": "Security & privacy",
|
"Security & privacy": "Security & privacy",
|
||||||
"All settings": "All settings",
|
"All settings": "All settings",
|
||||||
"Archived rooms": "Archived rooms",
|
|
||||||
"Feedback": "Feedback",
|
"Feedback": "Feedback",
|
||||||
"User menu": "User menu",
|
"User menu": "User menu",
|
||||||
"Could not load user profile": "Could not load user profile",
|
"Could not load user profile": "Could not load user profile",
|
||||||
|
|
|
@ -159,12 +159,6 @@ export const SETTINGS = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_irc_ui": {
|
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
|
||||||
displayName: _td('Enable IRC layout option in the appearance tab'),
|
|
||||||
default: false,
|
|
||||||
isFeature: true,
|
|
||||||
},
|
|
||||||
"mjolnirRooms": {
|
"mjolnirRooms": {
|
||||||
supportedLevels: ['account'],
|
supportedLevels: ['account'],
|
||||||
default: [],
|
default: [],
|
||||||
|
@ -574,7 +568,7 @@ export const SETTINGS = {
|
||||||
},
|
},
|
||||||
"useIRCLayout": {
|
"useIRCLayout": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("Use IRC layout"),
|
displayName: _td("Enable experimental, compact IRC style layout"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -125,6 +125,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async appendRoom(room: Room) {
|
private async appendRoom(room: Room) {
|
||||||
|
let updated = false;
|
||||||
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
||||||
|
|
||||||
// If the room is upgraded, use that room instead. We'll also splice out
|
// If the room is upgraded, use that room instead. We'll also splice out
|
||||||
|
@ -136,25 +137,36 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
// Take out any room that isn't the most recent room
|
// Take out any room that isn't the most recent room
|
||||||
for (let i = 0; i < history.length - 1; i++) {
|
for (let i = 0; i < history.length - 1; i++) {
|
||||||
const idx = rooms.findIndex(r => r.roomId === history[i].roomId);
|
const idx = rooms.findIndex(r => r.roomId === history[i].roomId);
|
||||||
if (idx !== -1) rooms.splice(idx, 1);
|
if (idx !== -1) {
|
||||||
|
rooms.splice(idx, 1);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the existing room, if it is present
|
// Remove the existing room, if it is present
|
||||||
const existingIdx = rooms.findIndex(r => r.roomId === room.roomId);
|
const existingIdx = rooms.findIndex(r => r.roomId === room.roomId);
|
||||||
|
|
||||||
|
// If we're focusing on the first room no-op
|
||||||
|
if (existingIdx !== 0) {
|
||||||
if (existingIdx !== -1) {
|
if (existingIdx !== -1) {
|
||||||
rooms.splice(existingIdx, 1);
|
rooms.splice(existingIdx, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Splice the room to the start of the list
|
// Splice the room to the start of the list
|
||||||
rooms.splice(0, 0, room);
|
rooms.splice(0, 0, room);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (rooms.length > MAX_ROOMS) {
|
if (rooms.length > MAX_ROOMS) {
|
||||||
// This looks weird, but it's saying to start at the MAX_ROOMS point in the
|
// This looks weird, but it's saying to start at the MAX_ROOMS point in the
|
||||||
// list and delete everything after it.
|
// list and delete everything after it.
|
||||||
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
|
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
|
||||||
|
updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
// Update the breadcrumbs
|
// Update the breadcrumbs
|
||||||
await this.updateState({rooms});
|
await this.updateState({rooms});
|
||||||
const roomIds = rooms.map(r => r.roomId);
|
const roomIds = rooms.map(r => r.roomId);
|
||||||
|
@ -162,5 +174,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
|
await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,23 +14,20 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "./INotificationState";
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
import { NotificationColor } from "./NotificationColor";
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
|
||||||
import { TagID } from "../room-list/models";
|
import { TagID } from "../room-list/models";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { arrayDiff } from "../../utils/arrays";
|
import { arrayDiff } from "../../utils/arrays";
|
||||||
import { RoomNotificationState } from "./RoomNotificationState";
|
import { RoomNotificationState } from "./RoomNotificationState";
|
||||||
import { TagSpecificNotificationState } from "./TagSpecificNotificationState";
|
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "./NotificationState";
|
||||||
|
|
||||||
export class ListNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
export type FetchRoomFn = (room: Room) => RoomNotificationState;
|
||||||
private _count: number;
|
|
||||||
private _color: NotificationColor;
|
export class ListNotificationState extends NotificationState {
|
||||||
private rooms: Room[] = [];
|
private rooms: Room[] = [];
|
||||||
private states: { [roomId: string]: RoomNotificationState } = {};
|
private states: { [roomId: string]: RoomNotificationState } = {};
|
||||||
|
|
||||||
constructor(private byTileCount = false, private tagId: TagID) {
|
constructor(private byTileCount = false, private tagId: TagID, private getRoomFn: FetchRoomFn) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,14 +35,6 @@ export class ListNotificationState extends EventEmitter implements IDestroyable,
|
||||||
return null; // This notification state doesn't support symbols
|
return null; // This notification state doesn't support symbols
|
||||||
}
|
}
|
||||||
|
|
||||||
public get count(): number {
|
|
||||||
return this._count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get color(): NotificationColor {
|
|
||||||
return this._color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setRooms(rooms: Room[]) {
|
public setRooms(rooms: Room[]) {
|
||||||
// If we're only concerned about the tile count, don't bother setting up listeners.
|
// If we're only concerned about the tile count, don't bother setting up listeners.
|
||||||
if (this.byTileCount) {
|
if (this.byTileCount) {
|
||||||
|
@ -62,10 +51,9 @@ export class ListNotificationState extends EventEmitter implements IDestroyable,
|
||||||
if (!state) continue; // We likely just didn't have a badge (race condition)
|
if (!state) continue; // We likely just didn't have a badge (race condition)
|
||||||
delete this.states[oldRoom.roomId];
|
delete this.states[oldRoom.roomId];
|
||||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||||
state.destroy();
|
|
||||||
}
|
}
|
||||||
for (const newRoom of diff.added) {
|
for (const newRoom of diff.added) {
|
||||||
const state = new TagSpecificNotificationState(newRoom, this.tagId);
|
const state = this.getRoomFn(newRoom);
|
||||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||||
if (this.states[newRoom.roomId]) {
|
if (this.states[newRoom.roomId]) {
|
||||||
// "Should never happen" disclaimer.
|
// "Should never happen" disclaimer.
|
||||||
|
@ -85,8 +73,9 @@ export class ListNotificationState extends EventEmitter implements IDestroyable,
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
|
super.destroy();
|
||||||
for (const state of Object.values(this.states)) {
|
for (const state of Object.values(this.states)) {
|
||||||
state.destroy();
|
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||||
}
|
}
|
||||||
this.states = {};
|
this.states = {};
|
||||||
}
|
}
|
||||||
|
@ -96,7 +85,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable,
|
||||||
};
|
};
|
||||||
|
|
||||||
private calculateTotalState() {
|
private calculateTotalState() {
|
||||||
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
const snapshot = this.snapshot();
|
||||||
|
|
||||||
if (this.byTileCount) {
|
if (this.byTileCount) {
|
||||||
this._color = NotificationColor.Red;
|
this._color = NotificationColor.Red;
|
||||||
|
@ -111,10 +100,7 @@ export class ListNotificationState extends EventEmitter implements IDestroyable,
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, publish an update if needed
|
// finally, publish an update if needed
|
||||||
const after = {count: this.count, symbol: this.symbol, color: this.color};
|
this.emitIfUpdated(snapshot);
|
||||||
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
||||||
this.emit(NOTIFICATION_STATE_UPDATE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
87
src/stores/notifications/NotificationState.ts
Normal file
87
src/stores/notifications/NotificationState.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 { EventEmitter } from "events";
|
||||||
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
|
||||||
|
export const NOTIFICATION_STATE_UPDATE = "update";
|
||||||
|
|
||||||
|
export abstract class NotificationState extends EventEmitter implements IDestroyable {
|
||||||
|
protected _symbol: string;
|
||||||
|
protected _count: number;
|
||||||
|
protected _color: NotificationColor;
|
||||||
|
|
||||||
|
public get symbol(): string {
|
||||||
|
return this._symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get count(): number {
|
||||||
|
return this._count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get color(): NotificationColor {
|
||||||
|
return this._color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isIdle(): boolean {
|
||||||
|
return this.color <= NotificationColor.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isUnread(): boolean {
|
||||||
|
return this.color >= NotificationColor.Bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasUnreadCount(): boolean {
|
||||||
|
return this.color >= NotificationColor.Grey && (!!this.count || !!this.symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasMentions(): boolean {
|
||||||
|
return this.color >= NotificationColor.Red;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected emitIfUpdated(snapshot: NotificationStateSnapshot) {
|
||||||
|
if (snapshot.isDifferentFrom(this)) {
|
||||||
|
this.emit(NOTIFICATION_STATE_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected snapshot(): NotificationStateSnapshot {
|
||||||
|
return new NotificationStateSnapshot(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.removeAllListeners(NOTIFICATION_STATE_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationStateSnapshot {
|
||||||
|
private readonly symbol: string;
|
||||||
|
private readonly count: number;
|
||||||
|
private readonly color: NotificationColor;
|
||||||
|
|
||||||
|
constructor(state: NotificationState) {
|
||||||
|
this.symbol = state.symbol;
|
||||||
|
this.count = state.count;
|
||||||
|
this.color = state.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isDifferentFrom(other: NotificationState): boolean {
|
||||||
|
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
||||||
|
const after = {count: other.count, symbol: other.symbol, color: other.color};
|
||||||
|
return JSON.stringify(before) !== JSON.stringify(after);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "./INotificationState";
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
import { NotificationColor } from "./NotificationColor";
|
||||||
import { IDestroyable } from "../../utils/IDestroyable";
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
@ -25,12 +23,9 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import * as RoomNotifs from '../../RoomNotifs';
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
import * as Unread from '../../Unread';
|
import * as Unread from '../../Unread';
|
||||||
|
import { NotificationState } from "./NotificationState";
|
||||||
|
|
||||||
export class RoomNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
export class RoomNotificationState extends NotificationState implements IDestroyable {
|
||||||
private _symbol: string;
|
|
||||||
private _count: number;
|
|
||||||
private _color: NotificationColor;
|
|
||||||
|
|
||||||
constructor(public readonly room: Room) {
|
constructor(public readonly room: Room) {
|
||||||
super();
|
super();
|
||||||
this.room.on("Room.receipt", this.handleReadReceipt);
|
this.room.on("Room.receipt", this.handleReadReceipt);
|
||||||
|
@ -41,23 +36,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable,
|
||||||
this.updateNotificationState();
|
this.updateNotificationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get symbol(): string {
|
|
||||||
return this._symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get count(): number {
|
|
||||||
return this._count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get color(): NotificationColor {
|
|
||||||
return this._color;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get roomIsInvite(): boolean {
|
private get roomIsInvite(): boolean {
|
||||||
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
|
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
|
super.destroy();
|
||||||
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
||||||
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||||
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||||
|
@ -87,7 +71,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable,
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateNotificationState() {
|
private updateNotificationState() {
|
||||||
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
const snapshot = this.snapshot();
|
||||||
|
|
||||||
if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
|
if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
|
||||||
// When muted we suppress all notification states, even if we have context on them.
|
// When muted we suppress all notification states, even if we have context on them.
|
||||||
|
@ -136,9 +120,6 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable,
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, publish an update if needed
|
// finally, publish an update if needed
|
||||||
const after = {count: this.count, symbol: this.symbol, color: this.color};
|
this.emitIfUpdated(snapshot);
|
||||||
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
||||||
this.emit(NOTIFICATION_STATE_UPDATE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
101
src/stores/notifications/RoomNotificationStateStore.ts
Normal file
101
src/stores/notifications/RoomNotificationStateStore.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { DefaultTagID, TagID } from "../room-list/models";
|
||||||
|
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomNotificationState } from "./RoomNotificationState";
|
||||||
|
import { TagSpecificNotificationState } from "./TagSpecificNotificationState";
|
||||||
|
|
||||||
|
const INSPECIFIC_TAG = "INSPECIFIC_TAG";
|
||||||
|
type INSPECIFIC_TAG = "INSPECIFIC_TAG";
|
||||||
|
|
||||||
|
interface IState {}
|
||||||
|
|
||||||
|
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
|
private static internalInstance = new RoomNotificationStateStore();
|
||||||
|
|
||||||
|
private roomMap = new Map<Room, Map<TagID | INSPECIFIC_TAG, RoomNotificationState>>();
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
super(defaultDispatcher, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new list notification state. The consumer is expected to set the rooms
|
||||||
|
* on the notification state, and destroy the state when it no longer needs it.
|
||||||
|
* @param tagId The tag to create the notification state for.
|
||||||
|
* @returns The notification state for the tag.
|
||||||
|
*/
|
||||||
|
public getListState(tagId: TagID): ListNotificationState {
|
||||||
|
// Note: we don't cache these notification states as the consumer is expected to call
|
||||||
|
// .setRooms() on the returned object, which could confuse other consumers.
|
||||||
|
|
||||||
|
// TODO: Update if/when invites move out of the room list.
|
||||||
|
const useTileCount = tagId === DefaultTagID.Invite;
|
||||||
|
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||||
|
return this.getRoomState(room, tagId);
|
||||||
|
};
|
||||||
|
return new ListNotificationState(useTileCount, tagId, getRoomFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a copy of the notification state for a room. The consumer should not
|
||||||
|
* attempt to destroy the returned state as it may be shared with other
|
||||||
|
* consumers.
|
||||||
|
* @param room The room to get the notification state for.
|
||||||
|
* @param inTagId Optional tag ID to scope the notification state to.
|
||||||
|
* @returns The room's notification state.
|
||||||
|
*/
|
||||||
|
public getRoomState(room: Room, inTagId?: TagID): RoomNotificationState {
|
||||||
|
if (!this.roomMap.has(room)) {
|
||||||
|
this.roomMap.set(room, new Map<TagID | INSPECIFIC_TAG, RoomNotificationState>());
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetTag = inTagId ? inTagId : INSPECIFIC_TAG;
|
||||||
|
|
||||||
|
const forRoomMap = this.roomMap.get(room);
|
||||||
|
if (!forRoomMap.has(targetTag)) {
|
||||||
|
if (inTagId) {
|
||||||
|
forRoomMap.set(inTagId, new TagSpecificNotificationState(room, inTagId));
|
||||||
|
} else {
|
||||||
|
forRoomMap.set(INSPECIFIC_TAG, new RoomNotificationState(room));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return forRoomMap.get(targetTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): RoomNotificationStateStore {
|
||||||
|
return RoomNotificationStateStore.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onNotReady(): Promise<any> {
|
||||||
|
for (const roomMap of this.roomMap.values()) {
|
||||||
|
for (const roomState of roomMap.values()) {
|
||||||
|
roomState.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need this, but our contract says we do.
|
||||||
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { INotificationState } from "./INotificationState";
|
|
||||||
import { NotificationColor } from "./NotificationColor";
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
import { NotificationState } from "./NotificationState";
|
||||||
|
|
||||||
export class StaticNotificationState extends EventEmitter implements INotificationState {
|
export class StaticNotificationState extends NotificationState {
|
||||||
constructor(public symbol: string, public count: number, public color: NotificationColor) {
|
constructor(symbol: string, count: number, color: NotificationColor) {
|
||||||
super();
|
super();
|
||||||
|
this._symbol = symbol;
|
||||||
|
this._count = count;
|
||||||
|
this._color = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static forCount(count: number, color: NotificationColor): StaticNotificationState {
|
public static forCount(count: number, color: NotificationColor): StaticNotificationState {
|
||||||
|
|
|
@ -92,11 +92,12 @@ export class ListLayout {
|
||||||
return 5 + RESIZER_BOX_FACTOR;
|
return 5 + RESIZER_BOX_FACTOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setVisibleTilesWithin(diff: number, maxPossible: number) {
|
public setVisibleTilesWithin(newVal: number, maxPossible: number) {
|
||||||
if (this.visibleTiles > maxPossible) {
|
maxPossible = maxPossible + RESIZER_BOX_FACTOR;
|
||||||
this.visibleTiles = maxPossible + diff;
|
if (newVal > maxPossible) {
|
||||||
|
this.visibleTiles = maxPossible;
|
||||||
} else {
|
} else {
|
||||||
this.visibleTiles += diff;
|
this.visibleTiles = newVal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,10 +112,6 @@ export class ListLayout {
|
||||||
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
|
return this.tilesToPixels(Math.min(maxTiles, n)) + padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
public tilesWithResizerBoxFactor(n: number): number {
|
|
||||||
return n + RESIZER_BOX_FACTOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
public tilesWithPadding(n: number, paddingPx: number): number {
|
public tilesWithPadding(n: number, paddingPx: number): number {
|
||||||
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
|
return this.pixelsToTiles(this.tilesToPixelsWithPadding(n, paddingPx));
|
||||||
}
|
}
|
||||||
|
|
73
src/stores/room-list/RoomListLayoutStore.ts
Normal file
73
src/stores/room-list/RoomListLayoutStore.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 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 { TagID } from "./models";
|
||||||
|
import { ListLayout } from "./ListLayout";
|
||||||
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
|
||||||
|
interface IState {}
|
||||||
|
|
||||||
|
export default class RoomListLayoutStore extends AsyncStoreWithClient<IState> {
|
||||||
|
private static internalInstance: RoomListLayoutStore;
|
||||||
|
|
||||||
|
private readonly layoutMap = new Map<TagID, ListLayout>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(defaultDispatcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): RoomListLayoutStore {
|
||||||
|
if (!RoomListLayoutStore.internalInstance) {
|
||||||
|
RoomListLayoutStore.internalInstance = new RoomListLayoutStore();
|
||||||
|
}
|
||||||
|
return RoomListLayoutStore.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ensureLayoutExists(tagId: TagID) {
|
||||||
|
if (!this.layoutMap.has(tagId)) {
|
||||||
|
this.layoutMap.set(tagId, new ListLayout(tagId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLayoutFor(tagId: TagID): ListLayout {
|
||||||
|
if (!this.layoutMap.has(tagId)) {
|
||||||
|
this.layoutMap.set(tagId, new ListLayout(tagId));
|
||||||
|
}
|
||||||
|
return this.layoutMap.get(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: this primarily exists for debugging, and isn't really intended to be used by anything.
|
||||||
|
public async resetLayouts() {
|
||||||
|
console.warn("Resetting layouts for room list");
|
||||||
|
for (const layout of this.layoutMap.values()) {
|
||||||
|
layout.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onNotReady(): Promise<any> {
|
||||||
|
// On logout, clear the map.
|
||||||
|
this.layoutMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need this function, but our contract says we do
|
||||||
|
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mx_RoomListLayoutStore = RoomListLayoutStore.instance;
|
|
@ -32,6 +32,7 @@ import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "./membership";
|
import { EffectiveMembership, getEffectiveMembership } from "./membership";
|
||||||
import { ListLayout } from "./ListLayout";
|
import { ListLayout } from "./ListLayout";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
|
import RoomListLayoutStore from "./RoomListLayoutStore";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -50,6 +51,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
private algorithm = new Algorithm();
|
private algorithm = new Algorithm();
|
||||||
private filterConditions: IFilterCondition[] = [];
|
private filterConditions: IFilterCondition[] = [];
|
||||||
private tagWatcher = new TagWatcher(this);
|
private tagWatcher = new TagWatcher(this);
|
||||||
|
private layoutMap: Map<TagID, ListLayout> = new Map<TagID, ListLayout>();
|
||||||
|
|
||||||
private readonly watchedSettings = [
|
private readonly watchedSettings = [
|
||||||
'feature_custom_tags',
|
'feature_custom_tags',
|
||||||
|
@ -416,6 +418,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
for (const tagId of OrderedDefaultTagIDs) {
|
for (const tagId of OrderedDefaultTagIDs) {
|
||||||
sorts[tagId] = this.calculateTagSorting(tagId);
|
sorts[tagId] = this.calculateTagSorting(tagId);
|
||||||
orders[tagId] = this.calculateListOrder(tagId);
|
orders[tagId] = this.calculateListOrder(tagId);
|
||||||
|
|
||||||
|
RoomListLayoutStore.instance.ensureLayoutExists(tagId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.tagsEnabled) {
|
if (this.state.tagsEnabled) {
|
||||||
|
@ -434,15 +438,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: this primarily exists for debugging, and isn't really intended to be used by anything.
|
|
||||||
public async resetLayouts() {
|
|
||||||
console.warn("Resetting layouts for room list");
|
|
||||||
for (const tagId of Object.keys(this.orderedLists)) {
|
|
||||||
new ListLayout(tagId).reset();
|
|
||||||
}
|
|
||||||
await this.regenerateAllLists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public addFilter(filter: IFilterCondition): void {
|
public addFilter(filter: IFilterCondition): void {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
console.log("Adding filter condition:", filter);
|
console.log("Adding filter condition:", filter);
|
||||||
|
|
|
@ -655,18 +655,35 @@ export class Algorithm extends EventEmitter {
|
||||||
cause = RoomUpdateCause.PossibleTagChange;
|
cause = RoomUpdateCause.PossibleTagChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have tags for a room and don't have the room referenced, the room reference
|
// Check to see if the room is known first
|
||||||
// probably changed. We need to swap out the problematic reference.
|
let knownRoomRef = this.rooms.includes(room);
|
||||||
if (hasTags && !this.rooms.includes(room) && !isSticky) {
|
if (hasTags && !knownRoomRef) {
|
||||||
console.warn(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
|
console.warn(`${room.roomId} might be a reference change - attempting to update reference`);
|
||||||
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
|
this.rooms = this.rooms.map(r => r.roomId === room.roomId ? room : r);
|
||||||
|
knownRoomRef = this.rooms.includes(room);
|
||||||
// Sanity check
|
if (!knownRoomRef) {
|
||||||
if (!this.rooms.includes(room)) {
|
console.warn(`${room.roomId} is still not referenced. It may be sticky.`);
|
||||||
throw new Error(`Failed to replace ${room.roomId} with an updated reference`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasTags && isForLastSticky && !knownRoomRef) {
|
||||||
|
// we have a fairly good chance at losing a room right now. Under some circumstances,
|
||||||
|
// we can end up with a room which transitions references and tag changes, then gets
|
||||||
|
// lost when the sticky room changes. To counter this, we try and add the room to the
|
||||||
|
// list manually as the condition below to update the reference will fail.
|
||||||
|
//
|
||||||
|
// Other conditions *should* result in the room being sorted into the right place.
|
||||||
|
console.warn(`${room.roomId} was about to be lost - inserting at end of room list`);
|
||||||
|
this.rooms.push(room);
|
||||||
|
knownRoomRef = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have tags for a room and don't have the room referenced, something went horribly
|
||||||
|
// wrong - the reference should have been updated above.
|
||||||
|
if (hasTags && !knownRoomRef && !isSticky) {
|
||||||
|
throw new Error(`${room.roomId} is missing from room array but is known - trying to find duplicate`);
|
||||||
|
}
|
||||||
|
|
||||||
// Like above, update the reference to the sticky room if we need to
|
// Like above, update the reference to the sticky room if we need to
|
||||||
if (hasTags && isSticky) {
|
if (hasTags && isSticky) {
|
||||||
// Go directly in and set the sticky room's new reference, being careful not
|
// Go directly in and set the sticky room's new reference, being careful not
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils";
|
||||||
import ReplyThread from "../../../components/views/elements/ReplyThread";
|
import ReplyThread from "../../../components/views/elements/ReplyThread";
|
||||||
|
import { sanitizedHtmlNodeInnerText } from "../../../HtmlUtils";
|
||||||
|
|
||||||
export class MessageEventPreview implements IPreview {
|
export class MessageEventPreview implements IPreview {
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
||||||
|
@ -36,14 +37,27 @@ export class MessageEventPreview implements IPreview {
|
||||||
const msgtype = eventContent['msgtype'];
|
const msgtype = eventContent['msgtype'];
|
||||||
if (!body || !msgtype) return null; // invalid event, no preview
|
if (!body || !msgtype) return null; // invalid event, no preview
|
||||||
|
|
||||||
|
const hasHtml = eventContent.format === "org.matrix.custom.html" && eventContent.formatted_body;
|
||||||
|
if (hasHtml) {
|
||||||
|
body = eventContent.formatted_body;
|
||||||
|
}
|
||||||
|
|
||||||
// XXX: Newer relations have a getRelation() function which is not compatible with replies.
|
// XXX: Newer relations have a getRelation() function which is not compatible with replies.
|
||||||
const mRelatesTo = event.getWireContent()['m.relates_to'];
|
const mRelatesTo = event.getWireContent()['m.relates_to'];
|
||||||
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
|
||||||
// If this is a reply, get the real reply and use that
|
// If this is a reply, get the real reply and use that
|
||||||
|
if (hasHtml) {
|
||||||
|
body = (ReplyThread.stripHTMLReply(body) || '').trim();
|
||||||
|
} else {
|
||||||
body = (ReplyThread.stripPlainReply(body) || '').trim();
|
body = (ReplyThread.stripPlainReply(body) || '').trim();
|
||||||
|
}
|
||||||
if (!body) return null; // invalid event, no preview
|
if (!body) return null; // invalid event, no preview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasHtml) {
|
||||||
|
body = sanitizedHtmlNodeInnerText(body);
|
||||||
|
}
|
||||||
|
|
||||||
if (msgtype === 'm.emote') {
|
if (msgtype === 'm.emote') {
|
||||||
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,8 +205,9 @@ describe("<TextualBody />", () => {
|
||||||
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
||||||
'Hey <span>' +
|
'Hey <span>' +
|
||||||
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
||||||
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
|
'<img src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" ' +
|
||||||
'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
|
'title="@member:domain.bla" alt="" aria-hidden="true" role="button" tabindex="0" ' +
|
||||||
|
'class="mx_AccessibleButton mx_BaseAvatar mx_BaseAvatar_image">Member</a>' +
|
||||||
'</span></span>');
|
'</span></span>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -1308,6 +1308,13 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
|
||||||
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
|
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
|
||||||
|
|
||||||
|
"@types/linkifyjs@^2.1.3":
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/linkifyjs/-/linkifyjs-2.1.3.tgz#80195c3c88c5e75d9f660e3046ce4a42be2c2fa4"
|
||||||
|
integrity sha512-V3Xt9wgaOvDPXcpOy3dC8qXCxy3cs0Lr/Hqgd9Bi6m3sf/vpbpTtfmVR0LJklrqYEjaAmc7e3Xh/INT2rCAKjQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/lodash@^4.14.152":
|
"@types/lodash@^4.14.152":
|
||||||
version "4.14.155"
|
version "4.14.155"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a"
|
||||||
|
@ -1372,6 +1379,13 @@
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
csstype "^2.2.0"
|
csstype "^2.2.0"
|
||||||
|
|
||||||
|
"@types/sanitize-html@^1.23.3":
|
||||||
|
version "1.23.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.23.3.tgz#26527783aba3bf195ad8a3c3e51bd3713526fc0d"
|
||||||
|
integrity sha512-Isg8N0ifKdDq6/kaNlIcWfapDXxxquMSk2XC5THsOICRyOIhQGds95XH75/PL/g9mExi4bL8otIqJM/Wo96WxA==
|
||||||
|
dependencies:
|
||||||
|
htmlparser2 "^4.1.0"
|
||||||
|
|
||||||
"@types/stack-utils@^1.0.1":
|
"@types/stack-utils@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
|
@ -2499,7 +2513,7 @@ class-utils@^0.3.5:
|
||||||
isobject "^3.0.0"
|
isobject "^3.0.0"
|
||||||
static-extend "^0.1.1"
|
static-extend "^0.1.1"
|
||||||
|
|
||||||
classnames@^2.1.2, classnames@^2.2.5:
|
classnames@^2.1.2:
|
||||||
version "2.2.6"
|
version "2.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
|
||||||
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
|
||||||
|
@ -3779,6 +3793,11 @@ fast-levenshtein@~2.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||||
|
|
||||||
|
fast-memoize@^2.5.1:
|
||||||
|
version "2.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
|
||||||
|
integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
|
||||||
|
|
||||||
fb-watchman@^2.0.0:
|
fb-watchman@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
|
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
|
||||||
|
@ -6882,7 +6901,7 @@ prop-types-exact@^1.2.0:
|
||||||
object.assign "^4.1.0"
|
object.assign "^4.1.0"
|
||||||
reflect.ownkeys "^0.2.0"
|
reflect.ownkeys "^0.2.0"
|
||||||
|
|
||||||
prop-types@15.x, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||||
version "15.7.2"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||||
|
@ -7053,6 +7072,13 @@ rc@1.2.8, rc@^1.2.8:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
re-resizable@^6.5.2:
|
||||||
|
version "6.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.5.2.tgz#7eb1928c673285d4dcf654211e47acb9a3801c3e"
|
||||||
|
integrity sha512-Pjo3ydkr/meTr6j3YZqyv+9fRS5UNOj5SaAI06gHFQ35BnpsZKmwNvupCnbo11gjQ1I62Uy+UzlHLO9xPQEuWQ==
|
||||||
|
dependencies:
|
||||||
|
fast-memoize "^2.5.1"
|
||||||
|
|
||||||
react-beautiful-dnd@^4.0.1:
|
react-beautiful-dnd@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
|
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
|
||||||
|
@ -7086,14 +7112,6 @@ react-dom@^16.9.0:
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.19.1"
|
scheduler "^0.19.1"
|
||||||
|
|
||||||
react-draggable@^4.0.3:
|
|
||||||
version "4.4.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.2.tgz#f3cefecee25f467f865144cda0d066e5f05f94a0"
|
|
||||||
integrity sha512-zLQs4R4bnBCGnCVTZiD8hPsHtkiJxgMpGDlRESM+EHQo8ysXhKJ2GKdJ8UxxLJdRVceX1j19jy+hQS2wHislPQ==
|
|
||||||
dependencies:
|
|
||||||
classnames "^2.2.5"
|
|
||||||
prop-types "^15.6.0"
|
|
||||||
|
|
||||||
react-focus-lock@^2.2.1:
|
react-focus-lock@^2.2.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47"
|
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.3.1.tgz#9d5d85899773609c7eefa4fc54fff6a0f5f2fc47"
|
||||||
|
@ -7138,14 +7156,6 @@ react-redux@^5.0.6:
|
||||||
react-is "^16.6.0"
|
react-is "^16.6.0"
|
||||||
react-lifecycles-compat "^3.0.0"
|
react-lifecycles-compat "^3.0.0"
|
||||||
|
|
||||||
react-resizable@^1.10.1:
|
|
||||||
version "1.10.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.10.1.tgz#f0c2cf1d83b3470b87676ce6d6b02bbe3f4d8cd4"
|
|
||||||
integrity sha512-Jd/bKOKx6+19NwC4/aMLRu/J9/krfxlDnElP41Oc+oLiUWs/zwV1S9yBfBZRnqAwQb6vQ/HRSk3bsSWGSgVbpw==
|
|
||||||
dependencies:
|
|
||||||
prop-types "15.x"
|
|
||||||
react-draggable "^4.0.3"
|
|
||||||
|
|
||||||
react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
|
react-test-renderer@^16.0.0-0, react-test-renderer@^16.9.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
|
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
|
||||||
|
|
Loading…
Reference in a new issue