mirror of
https://github.com/element-hq/element-web.git
synced 2024-12-16 20:41:46 +03:00
Merge branch 't3chguy/ts/8' into watch-show-timestamps
This commit is contained in:
commit
70bf47ced4
97 changed files with 2840 additions and 1523 deletions
16
package.json
16
package.json
|
@ -79,7 +79,7 @@
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"matrix-js-sdk": "12.0.0",
|
"matrix-js-sdk": "12.0.0",
|
||||||
"matrix-widget-api": "^0.1.0-beta.14",
|
"matrix-widget-api": "^0.1.0-beta.15",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-focus-lock": "^2.5.0",
|
"react-focus-lock": "^2.5.0",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
|
@ -123,7 +123,7 @@
|
||||||
"@sinonjs/fake-timers": "^7.0.2",
|
"@sinonjs/fake-timers": "^7.0.2",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/diff-match-patch": "^1.0.5",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/linkifyjs": "^2.1.3",
|
"@types/linkifyjs": "^2.1.3",
|
||||||
|
@ -133,8 +133,9 @@
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
"@types/parse5": "^6.0.0",
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^16.9",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^16.9.10",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
|
"@types/react-dom": "^17.0.2",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "^2.3.1",
|
"@types/sanitize-html": "^2.3.1",
|
||||||
"@types/zxcvbn": "^4.4.0",
|
"@types/zxcvbn": "^4.4.0",
|
||||||
|
@ -168,13 +169,10 @@
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
"walk": "^2.3.14"
|
"walk": "^2.3.14"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"**/@types/react": "^16.14"
|
|
||||||
},
|
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "./__test-utils__/environment.js",
|
"testEnvironment": "./__test-utils__/environment.js",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/test/**/*-test.[jt]s"
|
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||||
],
|
],
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"jest-canvas-mock"
|
"jest-canvas-mock"
|
||||||
|
|
|
@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
// Create another flexbox so the Panel fills the container
|
// Create another flexbox so the Panel fills the container
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.mx_SpacePanel_spaceTreeWrapper {
|
.mx_SpacePanel_spaceTreeWrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem_dragging {
|
||||||
|
.mx_SpaceButton_toggleCollapse {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceTreeLevel {
|
.mx_SpaceTreeLevel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -71,7 +71,7 @@ limitations under the License.
|
||||||
&::before {
|
&::before {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|
|
@ -38,6 +38,15 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/view-community.svg');
|
mask-image: url('$(res)/img/element-icons/view-community.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TagTileContextMenu_moveUp::before {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TagTileContextMenu_moveDown::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_TagTileContextMenu_hideCommunity::before {
|
.mx_TagTileContextMenu_hideCommunity::before {
|
||||||
mask-image: url('$(res)/img/element-icons/hide.svg');
|
mask-image: url('$(res)/img/element-icons/hide.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,6 +295,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_InviteDialog_content {
|
.mx_InviteDialog_content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,3 +317,42 @@ limitations under the License.
|
||||||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError {
|
||||||
|
> h4 {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
.mx_InviteDialog_multiInviterError_entry {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_userProfile {
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_name {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_userId {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_error {
|
||||||
|
margin-left: 32px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Not actually a component but things shared by settings components
|
// Not actually a component but things shared by settings components
|
||||||
.mx_UserSettingsDialog, .mx_RoomSettingsDialog {
|
.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
// set the height too since tabbed view scrolls itself.
|
// set the height too since tabbed view scrolls itself.
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_SpaceSettingsDialog {
|
.mx_SpaceSettingsDialog {
|
||||||
width: 480px;
|
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
.mx_SpaceSettings_errorText {
|
.mx_SpaceSettings_errorText {
|
||||||
|
@ -32,8 +31,44 @@ limitations under the License.
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_danger {
|
.mx_SettingsTab_section {
|
||||||
margin-top: 28px;
|
.mx_SettingsTab_section_caption {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_SettingsTab_subheading {
|
||||||
|
border-top: 1px solid $message-body-panel-bg-color;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.mx_RadioButton_content {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
margin-left: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsTab_showAdvanced {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsFlag {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceSettingsDialog_buttons {
|
.mx_SpaceSettingsDialog_buttons {
|
||||||
|
@ -52,4 +87,14 @@ limitations under the License.
|
||||||
.mx_AccessibleButton_hasKind {
|
.mx_AccessibleButton_hasKind {
|
||||||
padding: 8px 22px;
|
padding: 8px 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel {
|
||||||
|
.mx_SpaceSettingsDialog_generalIcon::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceSettingsDialog_visibilityIcon::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/eye.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_cryptoEvent_icon::after {
|
&.mx_cryptoEvent_icon::after {
|
||||||
|
|
|
@ -45,7 +45,7 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// transparent-looking border surrounding the shield for when overlain over avatars
|
// transparent-looking border surrounding the shield for when overlain over avatars
|
||||||
|
@ -59,7 +59,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
// shrink the infill of the badge
|
// shrink the infill of the badge
|
||||||
&::before {
|
&::before {
|
||||||
mask-size: 65%;
|
mask-size: 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -345,7 +345,7 @@ $hover-select-border: 4px;
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_SpaceBasicSettings {
|
.mx_SpaceBasicSettings {
|
||||||
.mx_Field {
|
.mx_Field {
|
||||||
margin: 32px 0;
|
margin: 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceBasicSettings_avatarContainer {
|
.mx_SpaceBasicSettings_avatarContainer {
|
||||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
||||||
.mx_DialPad_button {
|
.mx_DialPad_button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: $theme-button-bg-color;
|
background-color: $dialpad-button-bg-color;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -27,9 +27,22 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_dialled {
|
.mx_DialPadContextMenu_dialled {
|
||||||
height: 1em;
|
height: 1.5em;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
max-width: 150px;
|
||||||
|
border: none;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
.mx_DialPadContextMenu_dialled input {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 150px;
|
||||||
|
text-align: left;
|
||||||
|
direction: rtl;
|
||||||
|
padding: 8px 0px;
|
||||||
|
background-color: rgb(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_dialPad {
|
.mx_DialPadContextMenu_dialPad {
|
||||||
|
|
3
res/img/element-icons/eye.svg
Normal file
3
res/img/element-icons/eye.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 fill-rule="evenodd" clip-rule="evenodd" d="M14.3094 5.96587C15.3206 7.15704 15.3417 8.85457 14.3412 10.0548C13.0889 11.5571 10.9822 13.3332 8.02104 13.3332C5.05992 13.3332 2.9532 11.5571 1.70087 10.0548C0.700398 8.85457 0.721506 7.15704 1.7327 5.96587C3.01174 4.45918 5.1391 2.6665 8.02104 2.6665C10.903 2.6665 13.0303 4.45918 14.3094 5.96587ZM11.5556 7.99984C11.5556 9.96352 9.96369 11.5554 8.00001 11.5554C6.03633 11.5554 4.44446 9.96352 4.44446 7.99984C4.44446 6.03616 6.03633 4.44428 8.00001 4.44428C9.96369 4.44428 11.5556 6.03616 11.5556 7.99984ZM8.00001 9.77761C8.98185 9.77761 9.77779 8.98168 9.77779 7.99984C9.77779 7.018 8.98185 6.22206 8.00001 6.22206C7.01817 6.22206 6.22224 7.018 6.22224 7.99984C6.22224 8.98168 7.01817 9.77761 8.00001 9.77761Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 887 B |
|
@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #6F7882;
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $bg-color;
|
$roomlist-filter-active-bg-color: $bg-color;
|
||||||
|
|
|
@ -114,6 +114,8 @@ $voipcall-plinth-color: #394049;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #6F7882;
|
||||||
|
;
|
||||||
|
|
||||||
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
||||||
|
|
|
@ -181,6 +181,8 @@ $voipcall-plinth-color: #F4F6FA;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #e3e8f0;
|
||||||
|
|
||||||
|
|
||||||
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
||||||
|
|
|
@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #e3e8f0;
|
||||||
|
|
||||||
|
|
||||||
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: #ffffff;
|
$roomlist-filter-active-bg-color: #ffffff;
|
||||||
|
|
|
@ -15,20 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare module "diff-dom" {
|
declare module "diff-dom" {
|
||||||
enum Action {
|
|
||||||
AddElement = "addElement",
|
|
||||||
AddTextElement = "addTextElement",
|
|
||||||
RemoveTextElement = "removeTextElement",
|
|
||||||
RemoveElement = "removeElement",
|
|
||||||
ReplaceElement = "replaceElement",
|
|
||||||
ModifyTextElement = "modifyTextElement",
|
|
||||||
AddAttribute = "addAttribute",
|
|
||||||
RemoveAttribute = "removeAttribute",
|
|
||||||
ModifyAttribute = "modifyAttribute",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDiff {
|
export interface IDiff {
|
||||||
action: Action;
|
action: string;
|
||||||
name: string;
|
name: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
route: number[];
|
route: number[];
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
|
||||||
|
|
||||||
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 SettingsStore from "./settings/SettingsStore";
|
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
|
||||||
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
hasAnyLabeledDevices: async function() {
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
return devices.some(d => !!d.label);
|
|
||||||
},
|
|
||||||
|
|
||||||
getDevices: function() {
|
|
||||||
// Only needed for Electron atm, though should work in modern browsers
|
|
||||||
// once permission has been granted to the webapp
|
|
||||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
|
||||||
const audiooutput = [];
|
|
||||||
const audioinput = [];
|
|
||||||
const videoinput = [];
|
|
||||||
|
|
||||||
devices.forEach((device) => {
|
|
||||||
switch (device.kind) {
|
|
||||||
case 'audiooutput': audiooutput.push(device); break;
|
|
||||||
case 'audioinput': audioinput.push(device); break;
|
|
||||||
case 'videoinput': videoinput.push(device); break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log("Loaded WebRTC Devices", mediaDevices);
|
|
||||||
return {
|
|
||||||
audiooutput,
|
|
||||||
audioinput,
|
|
||||||
videoinput,
|
|
||||||
};
|
|
||||||
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
|
||||||
},
|
|
||||||
|
|
||||||
loadDevices: function() {
|
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
|
||||||
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setAudioOutput: function(deviceId) {
|
|
||||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setAudioInput: function(deviceId) {
|
|
||||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
|
||||||
setMatrixCallAudioInput(deviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setVideoInput: function(deviceId) {
|
|
||||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
|
||||||
setMatrixCallVideoInput(deviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
getAudioOutput: function() {
|
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
|
||||||
},
|
|
||||||
|
|
||||||
getAudioInput: function() {
|
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
|
||||||
},
|
|
||||||
|
|
||||||
getVideoInput: function() {
|
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -25,7 +25,7 @@ export class DecryptionFailure {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Fn = (count: number, trackedErrCode: string) => void;
|
type TrackingFn = (count: number, trackedErrCode: string) => void;
|
||||||
type ErrCodeMapFn = (errcode: string) => string;
|
type ErrCodeMapFn = (errcode: string) => string;
|
||||||
|
|
||||||
export class DecryptionFailureTracker {
|
export class DecryptionFailureTracker {
|
||||||
|
@ -73,7 +73,7 @@ export class DecryptionFailureTracker {
|
||||||
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
||||||
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
||||||
*/
|
*/
|
||||||
constructor(private readonly fn: Fn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
|
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
|
||||||
if (!fn || typeof fn !== 'function') {
|
if (!fn || typeof fn !== 'function') {
|
||||||
throw new Error('DecryptionFailureTracker requires tracking function');
|
throw new Error('DecryptionFailureTracker requires tracking function');
|
||||||
}
|
}
|
||||||
|
|
|
@ -505,7 +505,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||||
* @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: string, options = linkifyMatrix.options) {
|
export function linkifyString(str: string, options = linkifyMatrix.options): string {
|
||||||
return _linkifyString(str, options);
|
return _linkifyString(str, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,7 +516,7 @@ export function linkifyString(str: string, 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: HTMLElement, options = linkifyMatrix.options) {
|
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement {
|
||||||
return _linkifyElement(element, options);
|
return _linkifyElement(element, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
120
src/MediaDeviceHandler.ts
Normal file
120
src/MediaDeviceHandler.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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 SettingsStore from "./settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
|
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
interface IMediaDevices {
|
||||||
|
audioOutput: Array<MediaDeviceInfo>;
|
||||||
|
audioInput: Array<MediaDeviceInfo>;
|
||||||
|
videoInput: Array<MediaDeviceInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaDeviceHandlerEvent {
|
||||||
|
AudioOutputChanged = "audio_output_changed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MediaDeviceHandler extends EventEmitter {
|
||||||
|
private static internalInstance;
|
||||||
|
|
||||||
|
public static get instance(): MediaDeviceHandler {
|
||||||
|
if (!MediaDeviceHandler.internalInstance) {
|
||||||
|
MediaDeviceHandler.internalInstance = new MediaDeviceHandler();
|
||||||
|
}
|
||||||
|
return MediaDeviceHandler.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async hasAnyLabeledDevices(): Promise<boolean> {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.some(d => Boolean(d.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDevices(): Promise<IMediaDevices> {
|
||||||
|
// Only needed for Electron atm, though should work in modern browsers
|
||||||
|
// once permission has been granted to the webapp
|
||||||
|
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
|
const audioOutput = [];
|
||||||
|
const audioInput = [];
|
||||||
|
const videoInput = [];
|
||||||
|
|
||||||
|
devices.forEach((device) => {
|
||||||
|
switch (device.kind) {
|
||||||
|
case 'audiooutput': audioOutput.push(device); break;
|
||||||
|
case 'audioinput': audioInput.push(device); break;
|
||||||
|
case 'videoinput': videoInput.push(device); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { audioOutput, audioInput, videoInput };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to refresh WebRTC Devices: ', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
|
||||||
|
*/
|
||||||
|
public static loadDevices(): void {
|
||||||
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
|
setMatrixCallAudioInput(audioDeviceId);
|
||||||
|
setMatrixCallVideoInput(videoDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAudioOutput(deviceId: string): void {
|
||||||
|
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will not change the device that a potential call uses. The call will
|
||||||
|
* need to be ended and started again for this change to take effect
|
||||||
|
* @param {string} deviceId
|
||||||
|
*/
|
||||||
|
public setAudioInput(deviceId: string): void {
|
||||||
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
setMatrixCallAudioInput(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will not change the device that a potential call uses. The call will
|
||||||
|
* need to be ended and started again for this change to take effect
|
||||||
|
* @param {string} deviceId
|
||||||
|
*/
|
||||||
|
public setVideoInput(deviceId: string): void {
|
||||||
|
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
setMatrixCallVideoInput(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAudioOutput(): string {
|
||||||
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAudioInput(): string {
|
||||||
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getVideoInput(): string {
|
||||||
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,15 +14,26 @@ 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 {MatrixClientPeg} from './MatrixClientPeg';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
|
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './';
|
import * as sdk from './';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
|
||||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore";
|
||||||
|
import BaseAvatar from "./components/views/avatars/BaseAvatar";
|
||||||
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
|
||||||
|
export interface IInviteResult {
|
||||||
|
states: CompletionStates;
|
||||||
|
inviter: MultiInviter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room
|
* Invites multiple addresses to a room
|
||||||
|
@ -32,15 +41,15 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||||
* no option to cancel.
|
* no option to cancel.
|
||||||
*
|
*
|
||||||
* @param {string} roomId The ID of the room to invite to
|
* @param {string} roomId The ID of the room to invite to
|
||||||
* @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||||
* @returns {Promise} Promise
|
* @returns {Promise} Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId, addrs) {
|
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
||||||
const inviter = new MultiInviter(roomId);
|
const inviter = new MultiInviter(roomId);
|
||||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog(initialText) {
|
export function showStartChatInviteDialog(initialText = ""): void {
|
||||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
|
@ -49,7 +58,7 @@ export function showStartChatInviteDialog(initialText) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showRoomInviteDialog(roomId, initialText = "") {
|
export function showRoomInviteDialog(roomId: string, initialText = ""): void {
|
||||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
"Invite Users", "", InviteDialog, {
|
"Invite Users", "", InviteDialog, {
|
||||||
|
@ -61,14 +70,14 @@ export function showRoomInviteDialog(roomId, initialText = "") {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showCommunityRoomInviteDialog(roomId, communityName) {
|
export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void {
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showCommunityInviteDialog(communityId) {
|
export function showCommunityInviteDialog(communityId: string): void {
|
||||||
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
||||||
if (chat) {
|
if (chat) {
|
||||||
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
||||||
|
@ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) {
|
||||||
* @param {MatrixEvent} event The event to check
|
* @param {MatrixEvent} event The event to check
|
||||||
* @returns {boolean} True if valid, false otherwise
|
* @returns {boolean} True if valid, false otherwise
|
||||||
*/
|
*/
|
||||||
export function isValid3pidInvite(event) {
|
export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||||
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
||||||
|
|
||||||
// any events without these keys are not valid 3pid invites, so we ignore them
|
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||||
|
@ -96,7 +105,7 @@ export function isValid3pidInvite(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId, userIds) {
|
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
showAnyInviteErrors(result.states, room, result.inviter);
|
showAnyInviteErrors(result.states, room, result.inviter);
|
||||||
|
@ -110,9 +119,14 @@ export function inviteUsersToRoom(roomId, userIds) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showAnyInviteErrors(addrs, room, inviter) {
|
export function showAnyInviteErrors(
|
||||||
|
states: CompletionStates,
|
||||||
|
room: Room,
|
||||||
|
inviter: MultiInviter,
|
||||||
|
userMap?: Map<string, Member>,
|
||||||
|
): boolean {
|
||||||
// Show user any errors
|
// Show user any errors
|
||||||
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
const failedUsers = Object.keys(states).filter(a => states[a] === 'error');
|
||||||
if (failedUsers.length === 1 && inviter.fatal) {
|
if (failedUsers.length === 1 && inviter.fatal) {
|
||||||
// Just get the first message because there was a fatal problem on the first
|
// Just get the first message because there was a fatal problem on the first
|
||||||
// user. This usually means that no other users were attempted, making it
|
// user. This usually means that no other users were attempted, making it
|
||||||
|
@ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) {
|
||||||
} else {
|
} else {
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
for (const addr of failedUsers) {
|
for (const addr of failedUsers) {
|
||||||
if (addrs[addr] === "error") {
|
if (states[addr] === "error") {
|
||||||
const reason = inviter.getErrorText(addr);
|
const reason = inviter.getErrorText(addr);
|
||||||
errorList.push(addr + ": " + reason);
|
errorList.push(addr + ": " + reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
||||||
const description = <div>{errorList.map(e => <div key={e}>{e}</div>)}</div>;
|
const description = <div className="mx_InviteDialog_multiInviterError">
|
||||||
|
<h4>{ _t("We sent the others, but the below people couldn't be invited to <RoomName/>", {}, {
|
||||||
|
RoomName: () => <b>{ room.name }</b>,
|
||||||
|
}) }</h4>
|
||||||
|
<div>
|
||||||
|
{ failedUsers.map(addr => {
|
||||||
|
const user = userMap?.get(addr) || cli.getUser(addr);
|
||||||
|
const name = (user as Member).name || (user as User).rawDisplayName;
|
||||||
|
const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl;
|
||||||
|
return <div key={addr} className="mx_InviteDialog_multiInviterError_entry">
|
||||||
|
<div className="mx_InviteDialog_multiInviterError_entry_userProfile">
|
||||||
|
<BaseAvatar
|
||||||
|
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null}
|
||||||
|
name={name}
|
||||||
|
idName={user.userId}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span className="mx_InviteDialog_multiInviterError_entry_name">{ name }</span>
|
||||||
|
<span className="mx_InviteDialog_multiInviterError_entry_userId">{ user.userId }</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_InviteDialog_multiInviterError_entry_error">
|
||||||
|
{ inviter.getErrorText(addr) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, {
|
||||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
title: _t("Some invites couldn't be sent"),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
|
@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces
|
||||||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||||
|
|
||||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||||
// only meant to act as a cache to avoid prompting the user multiple times
|
// only meant to act as a cache to avoid prompting the user multiple times
|
||||||
|
@ -244,7 +245,7 @@ async function onSecretRequested(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
name: string,
|
name: string,
|
||||||
deviceTrust: IDeviceTrustLevel,
|
deviceTrust: DeviceTrustLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -31,76 +31,89 @@ function textForMemberEvent(ev): () => string | null {
|
||||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
|
const reason = content.reason;
|
||||||
|
|
||||||
const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': {
|
case 'invite': {
|
||||||
const threePidContent = content.third_party_invite;
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
return () => _t('%(targetName)s accepted the invitation for %(displayName)s', {
|
||||||
targetName,
|
targetName,
|
||||||
displayName: threePidContent.display_name,
|
displayName: threePidContent.display_name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(targetName)s accepted an invitation.', {targetName});
|
return () => _t('%(targetName)s accepted an invitation', { targetName });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
return () => reason
|
||||||
|
? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason })
|
||||||
|
: _t('%(senderName)s banned %(targetName)s', { senderName, targetName });
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent && prevContent.membership === 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', {
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (!prevContent.displayname && content.displayname) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return () => _t('%(senderName)s set their display name to %(displayName)s.', {
|
return () => _t('%(senderName)s set their display name to %(displayName)s', {
|
||||||
senderName: ev.getSender(),
|
senderName: ev.getSender(),
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.displayname && !content.displayname) {
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', {
|
||||||
senderName,
|
senderName,
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
return () => _t('%(senderName)s removed their profile picture.', {senderName});
|
return () => _t('%(senderName)s removed their profile picture', { senderName });
|
||||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
prevContent.avatar_url !== content.avatar_url) {
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
return () => _t('%(senderName)s changed their profile picture.', {senderName});
|
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
||||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
return () => _t('%(senderName)s set a profile picture.', {senderName});
|
return () => _t('%(senderName)s set a profile picture', { senderName });
|
||||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
||||||
return () => _t("%(senderName)s made no change.", {senderName});
|
return () => _t("%(senderName)s made no change", { senderName });
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
return () => _t('%(targetName)s joined the room.', {targetName});
|
return () => _t('%(targetName)s joined the room', { targetName });
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (prevContent.membership === "invite") {
|
if (prevContent.membership === "invite") {
|
||||||
return () => _t('%(targetName)s rejected the invitation.', {targetName});
|
return () => _t('%(targetName)s rejected the invitation', { targetName });
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(targetName)s left the room.', {targetName});
|
return () => reason
|
||||||
|
? _t('%(targetName)s left the room: %(reason)s', { targetName, reason })
|
||||||
|
: _t('%(targetName)s left the room', { targetName });
|
||||||
}
|
}
|
||||||
} else if (prevContent.membership === "ban") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName });
|
||||||
} else if (prevContent.membership === "invite") {
|
} else if (prevContent.membership === "invite") {
|
||||||
return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
return () => reason
|
||||||
senderName,
|
? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', {
|
||||||
targetName,
|
senderName,
|
||||||
}) + ' ' + getReason();
|
targetName,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName })
|
||||||
} else if (prevContent.membership === "join") {
|
} else if (prevContent.membership === "join") {
|
||||||
return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
return () => reason
|
||||||
|
? _t('%(senderName)s kicked %(targetName)s: %(reason)s', {
|
||||||
|
senderName,
|
||||||
|
targetName,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
: _t('%(senderName)s kicked %(targetName)s', { senderName, targetName });
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,15 +14,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
|
||||||
export const addressTypes = [
|
|
||||||
'mx-user-id', 'mx-room-id', 'email',
|
export enum AddressType {
|
||||||
];
|
Email = "email",
|
||||||
|
MatrixUserId = "mx-user-id",
|
||||||
|
MatrixRoomId = "mx-room-id",
|
||||||
|
}
|
||||||
|
|
||||||
// PropType definition for an object describing
|
// PropType definition for an object describing
|
||||||
// an address that can be invited to a room (which
|
// an address that can be invited to a room (which
|
||||||
|
@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({
|
||||||
isKnown: PropTypes.bool,
|
isKnown: PropTypes.bool,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText: string): AddressType | null {
|
||||||
const isEmailAddress = emailRegex.test(inputText);
|
if (emailRegex.test(inputText)) {
|
||||||
const isUserId = mxUserIdRegex.test(inputText);
|
return AddressType.Email;
|
||||||
const isRoomId = mxRoomIdRegex.test(inputText);
|
} else if (mxUserIdRegex.test(inputText)) {
|
||||||
|
return AddressType.MatrixUserId;
|
||||||
// sanity check the input for user IDs
|
} else if (mxRoomIdRegex.test(inputText)) {
|
||||||
if (isEmailAddress) {
|
return AddressType.MatrixRoomId;
|
||||||
return 'email';
|
|
||||||
} else if (isUserId) {
|
|
||||||
return 'mx-user-id';
|
|
||||||
} else if (isRoomId) {
|
|
||||||
return 'mx-room-id';
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
|
@ -55,13 +55,14 @@ const PROVIDERS = [
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
NotifProvider,
|
NotifProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
CommunityProvider,
|
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
||||||
if (SettingsStore.getValue("feature_spaces")) {
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
PROVIDERS.push(SpaceProvider);
|
PROVIDERS.push(SpaceProvider);
|
||||||
|
} else {
|
||||||
|
PROVIDERS.push(CommunityProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Providers will get rejected if they take longer than this.
|
// Providers will get rejected if they take longer than this.
|
||||||
|
|
|
@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { WheelEvent } from "react";
|
import React, { HTMLAttributes, WheelEvent } from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
||||||
className?: string;
|
className?: string;
|
||||||
onScroll?: (event: Event) => void;
|
onScroll?: (event: Event) => void;
|
||||||
onWheel?: (event: WheelEvent) => void;
|
onWheel?: (event: WheelEvent) => void;
|
||||||
|
@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
|
||||||
|
|
||||||
return (<div
|
return (<div
|
||||||
|
{...otherProps}
|
||||||
ref={this.containerRef}
|
ref={this.containerRef}
|
||||||
style={this.props.style}
|
style={style}
|
||||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||||
onWheel={this.props.onWheel}
|
onWheel={onWheel}
|
||||||
tabIndex={this.props.tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
{ this.props.children }
|
{ children }
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
import { Droppable } from 'react-beautiful-dnd';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMouseDown = e => {
|
onClick = e => {
|
||||||
// only dispatch if its not a no-op
|
// only dispatch if its not a no-op
|
||||||
if (this.state.selectedTags.length > 0) {
|
if (this.state.selectedTags.length > 0) {
|
||||||
dis.dispatch({action: 'deselect_tags'});
|
dis.dispatch({action: 'deselect_tags'});
|
||||||
|
@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
|
||||||
return <div className={classes} onClick={this.onClearFilterClick}>
|
return <div className={classes} onClick={this.onClearFilterClick}>
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
className="mx_GroupFilterPanel_scroller"
|
className="mx_GroupFilterPanel_scroller"
|
||||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
onClick={this.onClick}
|
||||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
|
|
||||||
onMouseDown={this.onMouseDown}
|
|
||||||
>
|
>
|
||||||
<Droppable
|
<div className="mx_GroupFilterPanel_tagTileContainer">
|
||||||
droppableId="tag-panel-droppable"
|
{ this.renderGlobalIcon() }
|
||||||
type="draggable-TagTile"
|
{ tags }
|
||||||
>
|
<div>
|
||||||
{ (provided, snapshot) => (
|
{ createButton }
|
||||||
<div
|
</div>
|
||||||
className="mx_GroupFilterPanel_tagTileContainer"
|
</div>
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
|
||||||
{ this.renderGlobalIcon() }
|
|
||||||
{ tags }
|
|
||||||
<div>
|
|
||||||
{createButton}
|
|
||||||
</div>
|
|
||||||
{ provided.placeholder }
|
|
||||||
</div>
|
|
||||||
) }
|
|
||||||
</Droppable>
|
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||||
|
|
||||||
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
||||||
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
||||||
const leftOverflowIndicator = this.props.trackHorizontalOverflow
|
const leftOverflowIndicator = trackHorizontalOverflow
|
||||||
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
||||||
const rightOverflowIndicator = this.props.trackHorizontalOverflow
|
const rightOverflowIndicator = trackHorizontalOverflow
|
||||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||||
|
|
||||||
return (<AutoHideScrollbar
|
return (<AutoHideScrollbar
|
||||||
ref={this._collectScrollerComponent}
|
ref={this._collectScrollerComponent}
|
||||||
wrappedRef={this._collectScroller}
|
wrappedRef={this._collectScroller}
|
||||||
onWheel={this.onMouseWheel}
|
onWheel={this.onMouseWheel}
|
||||||
{...this.props}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{ leftOverflowIndicator }
|
{ leftOverflowIndicator }
|
||||||
{ this.props.children }
|
{ children }
|
||||||
{ rightOverflowIndicator }
|
{ rightOverflowIndicator }
|
||||||
</AutoHideScrollbar>);
|
</AutoHideScrollbar>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,19 +19,16 @@ limitations under the License.
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as PropTypes from 'prop-types';
|
import * as PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
import {Key} from '../../Keyboard';
|
import {Key} from '../../Keyboard';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import CallMediaHandler from '../../CallMediaHandler';
|
import MediaDeviceHandler from '../../MediaDeviceHandler';
|
||||||
import { fixupColorFonts } from '../../utils/FontManager';
|
import { fixupColorFonts } from '../../utils/FontManager';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
|
||||||
import RoomListActions from '../../actions/RoomListActions';
|
|
||||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
import {Resizer, CollapseDistributor} from '../../resizer';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
@ -170,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
this._matrixClient = this.props.matrixClient;
|
this._matrixClient = this.props.matrixClient;
|
||||||
|
|
||||||
CallMediaHandler.loadDevices();
|
MediaDeviceHandler.loadDevices();
|
||||||
|
|
||||||
fixupColorFonts();
|
fixupColorFonts();
|
||||||
|
|
||||||
|
@ -569,50 +566,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDragEnd = (result) => {
|
|
||||||
// Dragged to an invalid destination, not onto a droppable
|
|
||||||
if (!result.destination) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dest = result.destination.droppableId;
|
|
||||||
|
|
||||||
if (dest === 'tag-panel-droppable') {
|
|
||||||
// Could be "GroupTile +groupId:domain"
|
|
||||||
const draggableId = result.draggableId.split(' ').pop();
|
|
||||||
|
|
||||||
// Dispatch synchronously so that the GroupFilterPanel receives an
|
|
||||||
// optimistic update from GroupFilterOrderStore before the previous
|
|
||||||
// state is shown.
|
|
||||||
dis.dispatch(TagOrderActions.moveTag(
|
|
||||||
this._matrixClient,
|
|
||||||
draggableId,
|
|
||||||
result.destination.index,
|
|
||||||
), true);
|
|
||||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
|
||||||
this._onRoomTileEndDrag(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_onRoomTileEndDrag = (result) => {
|
|
||||||
let newTag = result.destination.droppableId.split('_')[1];
|
|
||||||
let prevTag = result.source.droppableId.split('_')[1];
|
|
||||||
if (newTag === 'undefined') newTag = undefined;
|
|
||||||
if (prevTag === 'undefined') prevTag = undefined;
|
|
||||||
|
|
||||||
const roomId = result.draggableId.split('_')[1];
|
|
||||||
|
|
||||||
const oldIndex = result.source.index;
|
|
||||||
const newIndex = result.destination.index;
|
|
||||||
|
|
||||||
dis.dispatch(RoomListActions.tagRoom(
|
|
||||||
this._matrixClient,
|
|
||||||
this._matrixClient.getRoom(roomId),
|
|
||||||
prevTag, newTag,
|
|
||||||
oldIndex, newIndex,
|
|
||||||
), true);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
const UserView = sdk.getComponent('structures.UserView');
|
const UserView = sdk.getComponent('structures.UserView');
|
||||||
|
@ -679,17 +632,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
aria-hidden={this.props.hideToSRUsers}
|
aria-hidden={this.props.hideToSRUsers}
|
||||||
>
|
>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
<LeftPanel
|
||||||
<LeftPanel
|
isMinimized={this.props.collapseLhs || false}
|
||||||
isMinimized={this.props.collapseLhs || false}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
/>
|
||||||
/>
|
<ResizeHandle />
|
||||||
<ResizeHandle />
|
{ pageElement }
|
||||||
{ pageElement }
|
</div>
|
||||||
</div>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
</div>
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
|
|
|
@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
|
||||||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import ThemeController from "../../settings/controllers/ThemeController";
|
import ThemeController from "../../settings/controllers/ThemeController";
|
||||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
import { startAnyRegistrationFlow } from "../../Registration";
|
||||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||||
|
|
|
@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"To set up a filter, drag a community avatar over to the filter panel on " +
|
"You can click on an avatar in the " +
|
||||||
"the far left hand side of the screen. You can click on an avatar in the " +
|
|
||||||
"filter panel at any time to see only the rooms and people associated " +
|
"filter panel at any time to see only the rooms and people associated " +
|
||||||
"with that community.",
|
"with that community.",
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -336,11 +336,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||||
|
// If room was shift-clicked, remove it from the room directory
|
||||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.removeFromDirectory(room);
|
this.removeFromDirectory(room);
|
||||||
} else {
|
|
||||||
this.showRoom(room);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -567,11 +566,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
let avatarUrl = null;
|
let avatarUrl = null;
|
||||||
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||||
|
|
||||||
|
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
||||||
return [
|
return [
|
||||||
<div key={ `${room.room_id}_avatar` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_avatar` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_roomAvatar"
|
className="mx_RoomDirectory_roomAvatar"
|
||||||
>
|
>
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
|
@ -583,42 +582,50 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
url={avatarUrl}
|
url={avatarUrl}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_description` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_description` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_roomDescription"
|
className="mx_RoomDirectory_roomDescription"
|
||||||
>
|
>
|
||||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
<div
|
||||||
<div className="mx_RoomDirectory_topic"
|
className="mx_RoomDirectory_name"
|
||||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
>
|
||||||
|
{ name }
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx_RoomDirectory_topic"
|
||||||
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
dangerouslySetInnerHTML={{ __html: topic }}
|
dangerouslySetInnerHTML={{ __html: topic }}
|
||||||
/>
|
/>
|
||||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
<div
|
||||||
|
className="mx_RoomDirectory_alias"
|
||||||
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
>
|
||||||
|
{ getDisplayAliasForRoom(room) }
|
||||||
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_memberCount` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_memberCount` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_roomMemberCount"
|
className="mx_RoomDirectory_roomMemberCount"
|
||||||
>
|
>
|
||||||
{ room.num_joined_members }
|
{ room.num_joined_members }
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_preview` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_preview` }
|
||||||
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_preview"
|
className="mx_RoomDirectory_preview"
|
||||||
>
|
>
|
||||||
{previewButton}
|
{ previewButton }
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_join` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_join` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_join"
|
className="mx_RoomDirectory_join"
|
||||||
>
|
>
|
||||||
{joinOrViewButton}
|
{ joinOrViewButton }
|
||||||
</div>,
|
</div>,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {ReactNode, useMemo, useState} from "react";
|
import React, { ReactNode, useMemo, useState } from "react";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {sortBy} from "lodash";
|
import { sortBy } from "lodash";
|
||||||
|
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {_t} from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import SearchBox from "./SearchBox";
|
import SearchBox from "./SearchBox";
|
||||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
import {EnhancedMap} from "../../utils/maps";
|
import { EnhancedMap } from "../../utils/maps";
|
||||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||||
import {getOrder} from "../../stores/SpaceStore";
|
import { getChildOrder } from "../../stores/SpaceStore";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import {linkifyElement} from "../../HtmlUtils";
|
import { linkifyElement } from "../../HtmlUtils";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -286,7 +286,7 @@ export const HierarchyLevel = ({
|
||||||
const children = Array.from(relations.get(spaceId)?.values() || []);
|
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||||
const sortedChildren = sortBy(children, ev => {
|
const sortedChildren = sortBy(children, ev => {
|
||||||
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||||
return getOrder(ev.content.order, null, ev.state_key);
|
return getChildOrder(ev.content.order, null, ev.state_key);
|
||||||
});
|
});
|
||||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||||
const roomId = ev.state_key;
|
const roomId = ev.state_key;
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import Field from "../elements/Field";
|
||||||
import Dialpad from '../voip/DialPad';
|
import Dialpad from '../voip/DialPad';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
||||||
this.setState({value: this.state.value + digit});
|
this.setState({value: this.state.value + digit});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChange = (ev) => {
|
||||||
|
this.setState({value: ev.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <ContextMenu {...this.props}>
|
return <ContextMenu {...this.props}>
|
||||||
<div className="mx_DialPadContextMenu_header">
|
<div className="mx_DialPadContextMenu_header">
|
||||||
<div>
|
<div>
|
||||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div>
|
<Field className="mx_DialPadContextMenu_dialled"
|
||||||
|
value={this.state.value} autoFocus={true}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadContextMenu_horizSep" />
|
<div className="mx_DialPadContextMenu_horizSep" />
|
||||||
<div className="mx_DialPadContextMenu_dialPad">
|
<div className="mx_DialPadContextMenu_dialPad">
|
||||||
|
|
|
@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
pinnedIds.push(eventId);
|
pinnedIds.push(eventId);
|
||||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
event_ids: [
|
event_ids: [
|
||||||
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
|
||||||
eventId,
|
eventId,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import {MenuItem} from "../../structures/ContextMenu";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||||
export default class TagTileContextMenu extends React.Component {
|
export default class TagTileContextMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tag: PropTypes.string.isRequired,
|
tag: PropTypes.string.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
/* callback called when the menu is dismissed */
|
/* callback called when the menu is dismissed */
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor() {
|
_onViewCommunityClick = () => {
|
||||||
super();
|
|
||||||
|
|
||||||
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
|
||||||
this._onRemoveClick = this._onRemoveClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onViewCommunityClick() {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_group',
|
action: 'view_group',
|
||||||
group_id: this.props.tag,
|
group_id: this.props.tag,
|
||||||
});
|
});
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
};
|
||||||
|
|
||||||
_onRemoveClick() {
|
_onRemoveClick = () => {
|
||||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
_onMoveUp = () => {
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
_onMoveDown = () => {
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let moveUp;
|
||||||
|
let moveDown;
|
||||||
|
if (this.props.index > 0) {
|
||||||
|
moveUp = (
|
||||||
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
|
||||||
|
{ _t("Move up") }
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
|
||||||
|
moveDown = (
|
||||||
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
|
||||||
|
{ _t("Move down") }
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||||
{ _t('View Community') }
|
{ _t('View Community') }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" role="separator" /> : null }
|
||||||
|
{ moveUp }
|
||||||
|
{ moveDown }
|
||||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||||
{ _t('Hide') }
|
{ _t("Unpin") }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
import TruncatedList from "../elements/TruncatedList";
|
||||||
|
import EntityTile from "../rooms/EntityTile";
|
||||||
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
|
const [truncateAt, setTruncateAt] = useState(20);
|
||||||
|
function overflowTile(overflowCount, totalCount) {
|
||||||
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
|
return (
|
||||||
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||||
|
} name={text} presenceState="online" suppressOnHover={true}
|
||||||
|
onClick={() => setTruncateAt(totalCount)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_AddExistingToSpace">
|
return <div className="mx_AddExistingToSpace">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
{ rooms.length > 0 ? (
|
{ rooms.length > 0 ? (
|
||||||
<div className="mx_AddExistingToSpace_section">
|
<div className="mx_AddExistingToSpace_section">
|
||||||
<h3>{ _t("Rooms") }</h3>
|
<h3>{ _t("Rooms") }</h3>
|
||||||
{ rooms.map(room => {
|
<TruncatedList
|
||||||
return <Entry
|
truncateAt={truncateAt}
|
||||||
key={room.roomId}
|
createOverflowElement={overflowTile}
|
||||||
room={room}
|
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||||
checked={selectedToAdd.has(room)}
|
<Entry
|
||||||
onChange={onChange ? (checked) => {
|
key={room.roomId}
|
||||||
onChange(checked, room);
|
room={room}
|
||||||
} : null}
|
checked={selectedToAdd.has(room)}
|
||||||
/>;
|
onChange={onChange ? (checked) => {
|
||||||
}) }
|
onChange(checked, room);
|
||||||
|
} : null}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
getChildCount={() => rooms.length}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined }
|
) : undefined }
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import * as Email from '../../../email';
|
import * as Email from '../../../email';
|
||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
|
|
|
@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
|
||||||
render() {
|
render() {
|
||||||
const cli = this.context;
|
const cli = this.context;
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const inRoomChannel = cli.crypto._inRoomVerificationRequests;
|
const inRoomChannel = cli.crypto.inRoomVerificationRequests;
|
||||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
|
|
|
@ -40,6 +40,9 @@ import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||||
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
import TruncatedList from "../elements/TruncatedList";
|
||||||
|
import EntityTile from "../rooms/EntityTile";
|
||||||
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
|
|
||||||
const AVATAR_SIZE = 30;
|
const AVATAR_SIZE = 30;
|
||||||
|
|
||||||
|
@ -196,6 +199,17 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
}).match(lcQuery);
|
}).match(lcQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [truncateAt, setTruncateAt] = useState(20);
|
||||||
|
function overflowTile(overflowCount, totalCount) {
|
||||||
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
|
return (
|
||||||
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||||
|
} name={text} presenceState="online" suppressOnHover={true}
|
||||||
|
onClick={() => setTruncateAt(totalCount)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <BaseDialog
|
return <BaseDialog
|
||||||
title={_t("Forward message")}
|
title={_t("Forward message")}
|
||||||
className="mx_ForwardDialog"
|
className="mx_ForwardDialog"
|
||||||
|
@ -228,15 +242,20 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
<AutoHideScrollbar className="mx_ForwardList_content">
|
<AutoHideScrollbar className="mx_ForwardList_content">
|
||||||
{ rooms.length > 0 ? (
|
{ rooms.length > 0 ? (
|
||||||
<div className="mx_ForwardList_results">
|
<div className="mx_ForwardList_results">
|
||||||
{ rooms.map(room =>
|
<TruncatedList
|
||||||
<Entry
|
truncateAt={truncateAt}
|
||||||
key={room.roomId}
|
createOverflowElement={overflowTile}
|
||||||
room={room}
|
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||||
event={event}
|
<Entry
|
||||||
matrixClient={cli}
|
key={room.roomId}
|
||||||
onFinished={onFinished}
|
room={room}
|
||||||
/>,
|
event={event}
|
||||||
) }
|
matrixClient={cli}
|
||||||
|
onFinished={onFinished}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
getChildCount={() => rooms.length}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : <span className="mx_ForwardList_noResults">
|
) : <span className="mx_ForwardList_noResults">
|
||||||
{ _t("No results") }
|
{ _t("No results") }
|
||||||
|
|
|
@ -17,37 +17,45 @@ limitations under the License.
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import { _t, _td } from "../../../languageHandler";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import * as Email from "../../../email";
|
import * as Email from "../../../email";
|
||||||
import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
|
||||||
import {abbreviateUrl} from "../../../utils/UrlUtils";
|
import { abbreviateUrl } from "../../../utils/UrlUtils";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import { humanizeTime } from "../../../utils/humanize";
|
||||||
import createRoom, {
|
import createRoom, {
|
||||||
canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
|
canEncryptToAllUsers,
|
||||||
|
ensureDMExists,
|
||||||
|
findDMForUser,
|
||||||
|
privateShouldBeEncrypted,
|
||||||
} from "../../../createRoom";
|
} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
import {
|
||||||
import {Key} from "../../../Keyboard";
|
IInviteResult,
|
||||||
import {Action} from "../../../dispatcher/actions";
|
inviteMultipleToRoom,
|
||||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
showAnyInviteErrors,
|
||||||
|
showCommunityInviteDialog,
|
||||||
|
} from "../../../RoomInvite";
|
||||||
|
import { Key } from "../../../Keyboard";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import {getAddressType} from "../../../UserAddress";
|
import { getAddressType } from "../../../UserAddress";
|
||||||
import BaseAvatar from '../avatars/BaseAvatar';
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import { compare } from '../../../utils/strings';
|
import { compare } from '../../../utils/strings';
|
||||||
|
@ -74,10 +82,10 @@ export const KIND_CALL_TRANSFER = "call_transfer";
|
||||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||||
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
|
||||||
|
|
||||||
// This is the interface that is expected by various components in this file. It is a bit
|
// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
|
||||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
// for 3PIDs/email addresses.
|
// for 3PIDs/email addresses.
|
||||||
abstract class Member {
|
export abstract class Member {
|
||||||
/**
|
/**
|
||||||
* The display name of this Member. For users this should be their profile's display
|
* The display name of this Member. For users this should be their profile's display
|
||||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||||
|
@ -102,7 +110,8 @@ class DirectoryMember extends Member {
|
||||||
private readonly displayName: string;
|
private readonly displayName: string;
|
||||||
private readonly avatarUrl: string;
|
private readonly avatarUrl: string;
|
||||||
|
|
||||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
// eslint-disable-next-line camelcase
|
||||||
|
constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
|
||||||
super();
|
super();
|
||||||
this._userId = userDirResult.user_id;
|
this._userId = userDirResult.user_id;
|
||||||
this.displayName = userDirResult.display_name;
|
this.displayName = userDirResult.display_name;
|
||||||
|
@ -601,19 +610,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldAbortAfterInviteError(result): boolean {
|
private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
|
||||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
this.setState({ busy: false });
|
||||||
if (failedUsers.length > 0) {
|
const userMap = new Map<string, Member>(this.state.targets.map(member => [member.userId, member]));
|
||||||
console.log("Failed to invite users: ", result);
|
return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
|
||||||
this.setState({
|
|
||||||
busy: false,
|
|
||||||
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
|
|
||||||
csvUsers: failedUsers.join(", "),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
return true; // abort
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertFilter(): Member[] {
|
private convertFilter(): Member[] {
|
||||||
|
@ -731,7 +731,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
try {
|
try {
|
||||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||||
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
|
||||||
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
|
||||||
|
|
||||||
state: IState = {
|
state: IState = {
|
||||||
disabledButtonIds: [],
|
disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
|
||||||
|
.map(b => b.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
|
||||||
ROOM_ADVANCED_TAB,
|
ROOM_ADVANCED_TAB,
|
||||||
_td("Advanced"),
|
_td("Advanced"),
|
||||||
"mx_RoomSettingsDialog_warningIcon",
|
"mx_RoomSettingsDialog_warningIcon",
|
||||||
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />,
|
<AdvancedRoomSettingsTab
|
||||||
|
roomId={this.props.roomId}
|
||||||
|
closeSettingsFn={() => this.props.onFinished(true)}
|
||||||
|
/>,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
|
||||||
|
|
||||||
import {_t} from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import {IDialogProps} from "./IDialogProps";
|
import { IDialogProps } from "./IDialogProps";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DevtoolsDialog from "./DevtoolsDialog";
|
|
||||||
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
|
|
||||||
import {getTopic} from "../elements/RoomTopic";
|
|
||||||
import {avatarUrlForRoom} from "../../../Avatar";
|
|
||||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import Modal from "../../../Modal";
|
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import TabbedView, { Tab } from "../../structures/TabbedView";
|
||||||
|
import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
|
||||||
|
import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
|
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
|
||||||
|
|
||||||
|
export enum SpaceSettingsTab {
|
||||||
|
General = "SPACE_GENERAL_TAB",
|
||||||
|
Visibility = "SPACE_VISIBILITY_TAB",
|
||||||
|
Advanced = "SPACE_ADVANCED_TAB",
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const tabs = useMemo(() => {
|
||||||
const [error, setError] = useState("");
|
return [
|
||||||
|
new Tab(
|
||||||
const userId = cli.getUserId();
|
SpaceSettingsTab.General,
|
||||||
|
_td("General"),
|
||||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
"mx_SpaceSettingsDialog_generalIcon",
|
||||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
<SpaceSettingsGeneralTab matrixClient={cli} space={space} onFinished={onFinished} />,
|
||||||
const avatarChanged = newAvatar !== null;
|
),
|
||||||
|
new Tab(
|
||||||
const [name, setName] = useState<string>(space.name);
|
SpaceSettingsTab.Visibility,
|
||||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
_td("Visibility"),
|
||||||
const nameChanged = name !== space.name;
|
"mx_SpaceSettingsDialog_visibilityIcon",
|
||||||
|
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
|
||||||
const currentTopic = getTopic(space);
|
),
|
||||||
const [topic, setTopic] = useState<string>(currentTopic);
|
SettingsStore.getValue(UIFeature.AdvancedSettings)
|
||||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
? new Tab(
|
||||||
const topicChanged = topic !== currentTopic;
|
SpaceSettingsTab.Advanced,
|
||||||
|
_td("Advanced"),
|
||||||
const currentJoinRule = space.getJoinRule();
|
"mx_RoomSettingsDialog_warningIcon",
|
||||||
const [joinRule, setJoinRule] = useState(currentJoinRule);
|
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
|
||||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
)
|
||||||
const joinRuleChanged = joinRule !== currentJoinRule;
|
: null,
|
||||||
|
].filter(Boolean);
|
||||||
const onSave = async () => {
|
}, [cli, space, onFinished]);
|
||||||
setBusy(true);
|
|
||||||
const promises = [];
|
|
||||||
|
|
||||||
if (avatarChanged) {
|
|
||||||
if (newAvatar) {
|
|
||||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
|
||||||
url: await cli.uploadContent(newAvatar),
|
|
||||||
}, ""));
|
|
||||||
} else {
|
|
||||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nameChanged) {
|
|
||||||
promises.push(cli.setRoomName(space.roomId, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topicChanged) {
|
|
||||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (joinRuleChanged) {
|
|
||||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(promises);
|
|
||||||
setBusy(false);
|
|
||||||
const failures = results.filter(r => r.status === "rejected");
|
|
||||||
if (failures.length > 0) {
|
|
||||||
console.error("Failed to save space settings: ", failures);
|
|
||||||
setError(_t("Failed to save space settings."));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return <BaseDialog
|
return <BaseDialog
|
||||||
title={_t("Space settings")}
|
title={_t("Space settings")}
|
||||||
|
@ -110,61 +80,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
>
|
>
|
||||||
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
|
<div
|
||||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
className="mx_SpaceSettingsDialog_content"
|
||||||
|
id="mx_SpaceSettingsDialog"
|
||||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
title={_t("Settings - %(spaceName)s", { spaceName: space.name })}
|
||||||
|
>
|
||||||
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
<TabbedView tabs={tabs} />
|
||||||
|
|
||||||
<SpaceBasicSettings
|
|
||||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
|
||||||
avatarDisabled={busy || !canSetAvatar}
|
|
||||||
setAvatar={setNewAvatar}
|
|
||||||
name={name}
|
|
||||||
nameDisabled={busy || !canSetName}
|
|
||||||
setName={setName}
|
|
||||||
topic={topic}
|
|
||||||
topicDisabled={busy || !canSetTopic}
|
|
||||||
setTopic={setTopic}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{ _t("Make this space private") }
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={joinRule !== "public"}
|
|
||||||
onChange={checked => setJoinRule(checked ? "invite" : "public")}
|
|
||||||
disabled={!canSetJoinRule}
|
|
||||||
aria-label={_t("Make this space private")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AccessibleButton
|
|
||||||
kind="danger"
|
|
||||||
onClick={() => {
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: "leave_room",
|
|
||||||
room_id: space.roomId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ _t("Leave Space") }
|
|
||||||
</AccessibleButton>
|
|
||||||
|
|
||||||
<div className="mx_SpaceSettingsDialog_buttons">
|
|
||||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
|
||||||
{ _t("View dev tools") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
|
||||||
{ _t("Cancel") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
|
||||||
{ busy ? _t("Saving...") : _t("Save Changes") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SpaceSettingsDialog;
|
export default SpaceSettingsDialog;
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,8 @@ export default function AccessibleButton({
|
||||||
disabled,
|
disabled,
|
||||||
inputRef,
|
inputRef,
|
||||||
className,
|
className,
|
||||||
|
onKeyDown,
|
||||||
|
onKeyUp,
|
||||||
...restProps
|
...restProps
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
const newProps: IAccessibleButtonProps = restProps;
|
const newProps: IAccessibleButtonProps = restProps;
|
||||||
|
@ -83,6 +85,8 @@ export default function AccessibleButton({
|
||||||
if (e.key === Key.SPACE) {
|
if (e.key === Key.SPACE) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
onKeyDown?.(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
newProps.onKeyUp = (e) => {
|
newProps.onKeyUp = (e) => {
|
||||||
|
@ -94,6 +98,8 @@ export default function AccessibleButton({
|
||||||
if (e.key === Key.ENTER) {
|
if (e.key === Key.ENTER) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
} else {
|
||||||
|
onKeyUp?.(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { UserAddressType } from '../../../UserAddress.js';
|
import { UserAddressType } from '../../../UserAddress';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.AddressTile")
|
@replaceableComponent("views.elements.AddressTile")
|
||||||
export default class AddressTile extends React.Component {
|
export default class AddressTile extends React.Component {
|
||||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
||||||
import TagTile from './TagTile';
|
import TagTile from './TagTile';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
|
||||||
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
import { ContextMenu, toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
|
|
||||||
|
@ -31,32 +30,17 @@ export default function DNDTagTile(props) {
|
||||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
||||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} />
|
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} index={props.index} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <div>
|
return <>
|
||||||
<Draggable
|
<TagTile
|
||||||
key={props.tag}
|
{...props}
|
||||||
draggableId={props.tag}
|
contextMenuButtonRef={handle}
|
||||||
index={props.index}
|
menuDisplayed={menuDisplayed}
|
||||||
type="draggable-TagTile"
|
openMenu={openMenu}
|
||||||
>
|
/>
|
||||||
{(provided, snapshot) => (
|
|
||||||
<div
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
>
|
|
||||||
<TagTile
|
|
||||||
{...props}
|
|
||||||
contextMenuButtonRef={handle}
|
|
||||||
menuDisplayed={menuDisplayed}
|
|
||||||
openMenu={openMenu}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
{contextMenu}
|
{contextMenu}
|
||||||
</div>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017, 2019 New Vector Ltd.
|
Copyright 2017-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,48 +14,48 @@ 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 {_t} from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Field from "./Field";
|
import Field from "./Field";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
export class EditableItem extends React.Component {
|
interface IItemProps {
|
||||||
static propTypes = {
|
index?: number;
|
||||||
index: PropTypes.number,
|
value?: string;
|
||||||
value: PropTypes.string,
|
onRemove?(index: number): void;
|
||||||
onRemove: PropTypes.func,
|
}
|
||||||
|
|
||||||
|
interface IItemState {
|
||||||
|
verifyRemove: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditableItem extends React.Component<IItemProps, IItemState> {
|
||||||
|
public state = {
|
||||||
|
verifyRemove: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
private onRemove = (e) => {
|
||||||
super();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
verifyRemove: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRemove = (e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({verifyRemove: true});
|
this.setState({ verifyRemove: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDontRemove = (e) => {
|
private onDontRemove = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.setState({verifyRemove: false});
|
this.setState({ verifyRemove: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
_onActuallyRemove = (e) => {
|
private onActuallyRemove = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
if (this.props.onRemove) this.props.onRemove(this.props.index);
|
||||||
this.setState({verifyRemove: false});
|
this.setState({ verifyRemove: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -66,14 +66,14 @@ export class EditableItem extends React.Component {
|
||||||
{_t("Are you sure?")}
|
{_t("Are you sure?")}
|
||||||
</span>
|
</span>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onActuallyRemove}
|
onClick={this.onActuallyRemove}
|
||||||
kind="primary_sm"
|
kind="primary_sm"
|
||||||
className="mx_EditableItem_confirmBtn"
|
className="mx_EditableItem_confirmBtn"
|
||||||
>
|
>
|
||||||
{_t("Yes")}
|
{_t("Yes")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
onClick={this._onDontRemove}
|
onClick={this.onDontRemove}
|
||||||
kind="danger_sm"
|
kind="danger_sm"
|
||||||
className="mx_EditableItem_confirmBtn"
|
className="mx_EditableItem_confirmBtn"
|
||||||
>
|
>
|
||||||
|
@ -85,59 +85,68 @@ export class EditableItem extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_EditableItem">
|
<div className="mx_EditableItem">
|
||||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
<div onClick={this.onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
id: string;
|
||||||
|
items: string[];
|
||||||
|
itemsLabel?: string;
|
||||||
|
noItemsLabel?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
newItem?: string;
|
||||||
|
canEdit?: boolean;
|
||||||
|
canRemove?: boolean;
|
||||||
|
suggestionsListId?: string;
|
||||||
|
onItemAdded?(item: string): void;
|
||||||
|
onItemRemoved?(index: number): void;
|
||||||
|
onNewItemChanged?(item: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.EditableItemList")
|
@replaceableComponent("views.elements.EditableItemList")
|
||||||
export default class EditableItemList extends React.Component {
|
export default class EditableItemList<P = {}> extends React.PureComponent<IProps & P> {
|
||||||
static propTypes = {
|
protected onItemAdded = (e) => {
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
itemsLabel: PropTypes.string,
|
|
||||||
noItemsLabel: PropTypes.string,
|
|
||||||
placeholder: PropTypes.string,
|
|
||||||
newItem: PropTypes.string,
|
|
||||||
|
|
||||||
onItemAdded: PropTypes.func,
|
|
||||||
onItemRemoved: PropTypes.func,
|
|
||||||
onNewItemChanged: PropTypes.func,
|
|
||||||
|
|
||||||
canEdit: PropTypes.bool,
|
|
||||||
canRemove: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
_onItemAdded = (e) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onItemRemoved = (index) => {
|
protected onItemRemoved = (index) => {
|
||||||
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
if (this.props.onItemRemoved) this.props.onItemRemoved(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onNewItemChanged = (e) => {
|
protected onNewItemChanged = (e) => {
|
||||||
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderNewItemField() {
|
protected renderNewItemField() {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={this._onItemAdded}
|
onSubmit={this.onItemAdded}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
noValidate={true}
|
noValidate={true}
|
||||||
className="mx_EditableItemList_newItem"
|
className="mx_EditableItemList_newItem"
|
||||||
>
|
>
|
||||||
<Field label={this.props.placeholder} type="text"
|
<Field
|
||||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
label={this.props.placeholder}
|
||||||
list={this.props.suggestionsListId} />
|
type="text"
|
||||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit" disabled={!this.props.newItem}>
|
autoComplete="off"
|
||||||
{_t("Add")}
|
value={this.props.newItem || ""}
|
||||||
|
onChange={this.onNewItemChanged}
|
||||||
|
list={this.props.suggestionsListId}
|
||||||
|
/>
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={this.onItemAdded}
|
||||||
|
kind="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={!this.props.newItem}
|
||||||
|
>
|
||||||
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -153,19 +162,21 @@ export default class EditableItemList extends React.Component {
|
||||||
key={item}
|
key={item}
|
||||||
index={index}
|
index={index}
|
||||||
value={item}
|
value={item}
|
||||||
onRemove={this._onItemRemoved}
|
onRemove={this.onItemRemoved}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
const editableItemsSection = this.props.canRemove ? editableItems : <ul>{editableItems}</ul>;
|
||||||
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
const label = this.props.items.length > 0 ? this.props.itemsLabel : this.props.noItemsLabel;
|
||||||
|
|
||||||
return (<div className="mx_EditableItemList">
|
return (
|
||||||
<div className="mx_EditableItemList_label">
|
<div className="mx_EditableItemList">
|
||||||
{ label }
|
<div className="mx_EditableItemList_label">
|
||||||
|
{ label }
|
||||||
|
</div>
|
||||||
|
{ editableItemsSection }
|
||||||
|
{ this.props.canEdit ? this.renderNewItemField() : <div /> }
|
||||||
</div>
|
</div>
|
||||||
{ editableItemsSection }
|
);
|
||||||
{ this.props.canEdit ? this._renderNewItemField() : <div /> }
|
|
||||||
</div>);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -102,7 +102,8 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Fake it more
|
// Fake it more
|
||||||
event.sender = {
|
event.sender = {
|
||||||
name: this.props.displayName,
|
name: this.props.displayName || this.props.userId,
|
||||||
|
rawDisplayName: this.props.displayName,
|
||||||
userId: this.props.userId,
|
userId: this.props.userId,
|
||||||
getAvatarUrl: (..._) => {
|
getAvatarUrl: (..._) => {
|
||||||
return Avatar.avatarUrlForUser(
|
return Avatar.avatarUrlForUser(
|
||||||
|
|
|
@ -29,6 +29,11 @@ function getId() {
|
||||||
return `${BASE_ID}_${count++}`;
|
return `${BASE_ID}_${count++}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IValidateOpts {
|
||||||
|
focused?: boolean;
|
||||||
|
allowEmpty?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The field's ID, which binds the input and label together. Immutable.
|
// The field's ID, which binds the input and label together. Immutable.
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -180,7 +185,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
|
public async validate({ focused, allowEmpty = true }: IValidateOpts) {
|
||||||
if (!this.props.onValidate) {
|
if (!this.props.onValidate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,38 +14,33 @@ 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 ToggleSwitch from "./ToggleSwitch";
|
import ToggleSwitch from "./ToggleSwitch";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// The value for the toggle switch
|
||||||
|
value: boolean;
|
||||||
|
// The translated label for the switch
|
||||||
|
label: string;
|
||||||
|
// Whether or not to disable the toggle switch
|
||||||
|
disabled?: boolean;
|
||||||
|
// True to put the toggle in front of the label
|
||||||
|
// Default false.
|
||||||
|
toggleInFront?: boolean;
|
||||||
|
// Additional class names to append to the switch. Optional.
|
||||||
|
className?: string;
|
||||||
|
// The function to call when the value changes
|
||||||
|
onChange(checked: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.elements.LabelledToggleSwitch")
|
@replaceableComponent("views.elements.LabelledToggleSwitch")
|
||||||
export default class LabelledToggleSwitch extends React.Component {
|
export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
||||||
static propTypes = {
|
|
||||||
// The value for the toggle switch
|
|
||||||
value: PropTypes.bool.isRequired,
|
|
||||||
|
|
||||||
// The function to call when the value changes
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
|
|
||||||
// The translated label for the switch
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
|
|
||||||
// Whether or not to disable the toggle switch
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
|
|
||||||
// True to put the toggle in front of the label
|
|
||||||
// Default false.
|
|
||||||
toggleInFront: PropTypes.bool,
|
|
||||||
|
|
||||||
// Additional class names to append to the switch. Optional.
|
|
||||||
className: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// This is a minimal version of a SettingsFlag
|
// This is a minimal version of a SettingsFlag
|
||||||
|
|
||||||
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
|
let firstPart = <span className="mx_SettingsFlag_label">{ this.props.label }</span>;
|
||||||
let secondPart = <ToggleSwitch
|
let secondPart = <ToggleSwitch
|
||||||
checked={this.props.value}
|
checked={this.props.value}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -13,67 +13,78 @@ 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, { createRef } from "react";
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import withValidation from './Validation';
|
import withValidation from './Validation';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import Field, { IValidateOpts } from "./Field";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
domain: string;
|
||||||
|
value: string;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
onChange?(value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isValid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
|
||||||
@replaceableComponent("views.elements.RoomAliasField")
|
@replaceableComponent("views.elements.RoomAliasField")
|
||||||
export default class RoomAliasField extends React.PureComponent {
|
export default class RoomAliasField extends React.PureComponent<IProps, IState> {
|
||||||
static propTypes = {
|
private fieldRef = createRef<Field>();
|
||||||
domain: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props, context);
|
||||||
this.state = {isValid: true};
|
|
||||||
|
this.state = {
|
||||||
|
isValid: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_asFullAlias(localpart) {
|
private asFullAlias(localpart: string): string {
|
||||||
return `#${localpart}:${this.props.domain}`;
|
return `#${localpart}:${this.props.domain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Field = sdk.getComponent('views.elements.Field');
|
|
||||||
const poundSign = (<span>#</span>);
|
const poundSign = (<span>#</span>);
|
||||||
const aliasPostfix = ":" + this.props.domain;
|
const aliasPostfix = ":" + this.props.domain;
|
||||||
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
const domain = (<span title={aliasPostfix}>{aliasPostfix}</span>);
|
||||||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
label={_t("Room address")}
|
label={this.props.label || _t("Room address")}
|
||||||
className="mx_RoomAliasField"
|
className="mx_RoomAliasField"
|
||||||
prefixComponent={poundSign}
|
prefixComponent={poundSign}
|
||||||
postfixComponent={domain}
|
postfixComponent={domain}
|
||||||
ref={ref => this._fieldRef = ref}
|
ref={this.fieldRef}
|
||||||
onValidate={this._onValidate}
|
onValidate={this.onValidate}
|
||||||
placeholder={_t("e.g. my-room")}
|
placeholder={this.props.placeholder || _t("e.g. my-room")}
|
||||||
onChange={this._onChange}
|
onChange={this.onChange}
|
||||||
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
|
||||||
maxLength={maxlength}
|
maxLength={maxlength}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onChange = (ev) => {
|
private onChange = (ev) => {
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange(this._asFullAlias(ev.target.value));
|
this.props.onChange(this.asFullAlias(ev.target.value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onValidate = async (fieldState) => {
|
private onValidate = async (fieldState) => {
|
||||||
const result = await this._validationRules(fieldState);
|
const result = await this.validationRules(fieldState);
|
||||||
this.setState({isValid: result.valid});
|
this.setState({isValid: result.valid});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
_validationRules = withValidation({
|
private validationRules = withValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "safeLocalpart",
|
key: "safeLocalpart",
|
||||||
|
@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const fullAlias = this._asFullAlias(value);
|
const fullAlias = this.asFullAlias(value);
|
||||||
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
|
||||||
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
|
||||||
encodeURI(fullAlias) === fullAlias;
|
encodeURI(fullAlias) === fullAlias;
|
||||||
|
@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||||
}, {
|
}, {
|
||||||
key: "required",
|
key: "required",
|
||||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||||
invalid: () => _t("Please provide a room address"),
|
invalid: () => _t("Please provide an address"),
|
||||||
}, {
|
}, {
|
||||||
key: "taken",
|
key: "taken",
|
||||||
final: true,
|
final: true,
|
||||||
|
@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||||
}
|
}
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
try {
|
try {
|
||||||
await client.getRoomIdForAlias(this._asFullAlias(value));
|
await client.getRoomIdForAlias(this.asFullAlias(value));
|
||||||
// we got a room id, so the alias is taken
|
// we got a room id, so the alias is taken
|
||||||
return false;
|
return false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
get isValid() {
|
public get isValid() {
|
||||||
return this.state.isValid;
|
return this.state.isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate(options) {
|
public validate(options: IValidateOpts) {
|
||||||
return this._fieldRef.validate(options);
|
return this.fieldRef.current?.validate(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
public focus() {
|
||||||
this._fieldRef.focus();
|
this.fieldRef.current?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -34,10 +34,19 @@ interface IProps<T extends string> {
|
||||||
definitions: IDefinition<T>[];
|
definitions: IDefinition<T>[];
|
||||||
value?: T; // if not provided no options will be selected
|
value?: T; // if not provided no options will be selected
|
||||||
outlined?: boolean;
|
outlined?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
onChange(newValue: T): void;
|
onChange(newValue: T): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StyledRadioGroup<T extends string>({name, definitions, value, className, outlined, onChange}: IProps<T>) {
|
function StyledRadioGroup<T extends string>({
|
||||||
|
name,
|
||||||
|
definitions,
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
outlined,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
|
}: IProps<T>) {
|
||||||
const _onChange = e => {
|
const _onChange = e => {
|
||||||
onChange(e.target.value);
|
onChange(e.target.value);
|
||||||
};
|
};
|
||||||
|
@ -50,12 +59,12 @@ function StyledRadioGroup<T extends string>({name, definitions, value, className
|
||||||
checked={d.checked !== undefined ? d.checked : d.value === value}
|
checked={d.checked !== undefined ? d.checked : d.value === value}
|
||||||
name={name}
|
name={name}
|
||||||
value={d.value}
|
value={d.value}
|
||||||
disabled={d.disabled}
|
disabled={disabled || d.disabled}
|
||||||
outlined={outlined}
|
outlined={outlined}
|
||||||
>
|
>
|
||||||
{d.label}
|
{ d.label }
|
||||||
</StyledRadioButton>
|
</StyledRadioButton>
|
||||||
{d.description}
|
{ d.description ? <span>{ d.description }</span> : null }
|
||||||
</React.Fragment>)}
|
</React.Fragment>)}
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,31 +16,29 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.elements.TruncatedList")
|
interface IProps {
|
||||||
export default class TruncatedList extends React.Component {
|
// The number of elements to show before truncating. If negative, no truncation is done.
|
||||||
static propTypes = {
|
truncateAt?: number;
|
||||||
// The number of elements to show before truncating. If negative, no truncation is done.
|
// The className to apply to the wrapping div
|
||||||
truncateAt: PropTypes.number,
|
className?: string;
|
||||||
// The className to apply to the wrapping div
|
// A function that returns the children to be rendered into the element.
|
||||||
className: PropTypes.string,
|
// The start element is included, the end is not (as in `slice`).
|
||||||
// A function that returns the children to be rendered into the element.
|
// If omitted, the React child elements will be used. This parameter can be used
|
||||||
// function getChildren(start: number, end: number): Array<React.Node>
|
// to avoid creating unnecessary React elements.
|
||||||
// The start element is included, the end is not (as in `slice`).
|
getChildren?: (start: number, end: number) => Array<React.ReactNode>;
|
||||||
// If omitted, the React child elements will be used. This parameter can be used
|
// A function that should return the total number of child element available.
|
||||||
// to avoid creating unnecessary React elements.
|
// Required if getChildren is supplied.
|
||||||
getChildren: PropTypes.func,
|
getChildCount?: () => number;
|
||||||
// A function that should return the total number of child element available.
|
// A function which will be invoked when an overflow element is required.
|
||||||
// Required if getChildren is supplied.
|
// This will be inserted after the children.
|
||||||
getChildCount: PropTypes.func,
|
createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
|
||||||
// A function which will be invoked when an overflow element is required.
|
}
|
||||||
// This will be inserted after the children.
|
|
||||||
createOverflowElement: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
@replaceableComponent("views.elements.TruncatedList")
|
||||||
|
export default class TruncatedList extends React.Component<IProps> {
|
||||||
static defaultProps ={
|
static defaultProps ={
|
||||||
truncateAt: 2,
|
truncateAt: 2,
|
||||||
createOverflowElement(overflowCount, totalCount) {
|
createOverflowElement(overflowCount, totalCount) {
|
||||||
|
@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
_getChildren(start, end) {
|
private getChildren(start: number, end: number): Array<React.ReactNode> {
|
||||||
if (this.props.getChildren && this.props.getChildCount) {
|
if (this.props.getChildren && this.props.getChildCount) {
|
||||||
return this.props.getChildren(start, end);
|
return this.props.getChildren(start, end);
|
||||||
} else {
|
} else {
|
||||||
|
@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getChildCount() {
|
private getChildCount(): number {
|
||||||
if (this.props.getChildren && this.props.getChildCount) {
|
if (this.props.getChildren && this.props.getChildCount) {
|
||||||
return this.props.getChildCount();
|
return this.props.getChildCount();
|
||||||
} else {
|
} else {
|
||||||
|
@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
let overflowNode = null;
|
let overflowNode = null;
|
||||||
|
|
||||||
const totalChildren = this._getChildCount();
|
const totalChildren = this.getChildCount();
|
||||||
let upperBound = totalChildren;
|
let upperBound = totalChildren;
|
||||||
if (this.props.truncateAt >= 0) {
|
if (this.props.truncateAt >= 0) {
|
||||||
const overflowCount = totalChildren - this.props.truncateAt;
|
const overflowCount = totalChildren - this.props.truncateAt;
|
||||||
|
@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
|
||||||
upperBound = this.props.truncateAt;
|
upperBound = this.props.truncateAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const childNodes = this._getChildren(0, upperBound);
|
const childNodes = this.getChildren(0, upperBound);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.className}>
|
<div className={this.props.className}>
|
|
@ -66,9 +66,7 @@ export default class GroupPublicityToggle extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||||
return <div className="mx_GroupPublicity_toggle">
|
return <div className="mx_GroupPublicity_toggle">
|
||||||
<GroupTile groupId={this.props.groupId} showDescription={false}
|
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||||
avatarHeight={40} draggable={false}
|
|
||||||
/>
|
|
||||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||||
disabled={!this.state.ready || this.state.busy}
|
disabled={!this.state.ready || this.state.busy}
|
||||||
onChange={this._onPublicityToggle} />
|
onChange={this._onPublicityToggle} />
|
||||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
function nop() {}
|
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||||
|
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||||
|
|
||||||
@replaceableComponent("views.groups.GroupTile")
|
@replaceableComponent("views.groups.GroupTile")
|
||||||
class GroupTile extends React.Component {
|
class GroupTile extends React.Component {
|
||||||
|
@ -34,7 +34,6 @@ class GroupTile extends React.Component {
|
||||||
showDescription: PropTypes.bool,
|
showDescription: PropTypes.bool,
|
||||||
// Height of the group avatar in pixels
|
// Height of the group avatar in pixels
|
||||||
avatarHeight: PropTypes.number,
|
avatarHeight: PropTypes.number,
|
||||||
draggable: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
@ -42,7 +41,6 @@ class GroupTile extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
showDescription: true,
|
showDescription: true,
|
||||||
avatarHeight: 50,
|
avatarHeight: 50,
|
||||||
draggable: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -57,7 +55,7 @@ class GroupTile extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown = e => {
|
onClick = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_group',
|
action: 'view_group',
|
||||||
|
@ -65,6 +63,18 @@ class GroupTile extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onPinClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnpinClick = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
@ -78,7 +88,7 @@ class GroupTile extends React.Component {
|
||||||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
let avatarElement = (
|
const avatarElement = (
|
||||||
<div className="mx_GroupTile_avatar">
|
<div className="mx_GroupTile_avatar">
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -88,46 +98,21 @@ class GroupTile extends React.Component {
|
||||||
height={avatarHeight} />
|
height={avatarHeight} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
if (this.props.draggable) {
|
|
||||||
const avatarClone = avatarElement;
|
|
||||||
avatarElement = (
|
|
||||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
|
||||||
{ (droppableProvided, droppableSnapshot) => (
|
|
||||||
<div ref={droppableProvided.innerRef}>
|
|
||||||
<Draggable
|
|
||||||
key={"GroupTile " + this.props.groupId}
|
|
||||||
draggableId={"GroupTile " + this.props.groupId}
|
|
||||||
index={this.props.groupId}
|
|
||||||
type="draggable-TagTile"
|
|
||||||
>
|
|
||||||
{ (provided, snapshot) => (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
>
|
|
||||||
{avatarClone}
|
|
||||||
</div>
|
|
||||||
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
|
||||||
{ provided.placeholder ? avatarClone : <div /> }
|
|
||||||
</div>
|
|
||||||
) }
|
|
||||||
</Draggable>
|
|
||||||
</div>
|
|
||||||
) }
|
|
||||||
</Droppable>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6156
|
|
||||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
<div className="mx_GroupTile_profile">
|
<div className="mx_GroupTile_profile">
|
||||||
<div className="mx_GroupTile_name">{ name }</div>
|
<div className="mx_GroupTile_name">{ name }</div>
|
||||||
{ descElement }
|
{ descElement }
|
||||||
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
||||||
|
{ !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
|
||||||
|
? <AccessibleButton kind="link" onClick={this.onPinClick}>
|
||||||
|
{ _t("Pin") }
|
||||||
|
</AccessibleButton>
|
||||||
|
: <AccessibleButton kind="link" onClick={this.onUnpinClick}>
|
||||||
|
{ _t("Unpin") }
|
||||||
|
</AccessibleButton>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Flair from '../elements/Flair.js';
|
import Flair from '../elements/Flair';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
|
|
||||||
import React, {useCallback, useContext, useEffect, useState} from "react";
|
import React, {useCallback, useContext, useEffect, useState} from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
|
||||||
|
@ -28,6 +27,7 @@ import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import PinningUtils from "../../../utils/PinningUtils";
|
import PinningUtils from "../../../utils/PinningUtils";
|
||||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
import PinnedEventTile from "../rooms/PinnedEventTile";
|
import PinnedEventTile from "../rooms/PinnedEventTile";
|
||||||
|
import { useRoomState } from "../../../hooks/useRoomState";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -75,24 +75,6 @@ export const useReadPinnedEvents = (room: Room): Set<string> => {
|
||||||
return readPinnedEvents;
|
return readPinnedEvents;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useRoomState = <T extends any>(room: Room, mapper: (state: RoomState) => T): T => {
|
|
||||||
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
|
||||||
|
|
||||||
const update = useCallback(() => {
|
|
||||||
if (!room) return;
|
|
||||||
setValue(mapper(room.currentState));
|
|
||||||
}, [room, mapper]);
|
|
||||||
|
|
||||||
useEventEmitter(room?.currentState, "RoomState.events", update);
|
|
||||||
useEffect(() => {
|
|
||||||
update();
|
|
||||||
return () => {
|
|
||||||
setValue(undefined);
|
|
||||||
};
|
|
||||||
}, [update]);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
const PinnedMessagesCard = ({ room, onClose }: IProps) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
const canUnpin = useRoomState(room, state => state.mayClientSendStateEvent(EventType.RoomPinnedEvents, cli));
|
||||||
|
|
|
@ -503,19 +503,15 @@ const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) =>
|
||||||
return member.powerLevel < levelToSend;
|
return member.powerLevel < levelToSend;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPowerLevels = room => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||||
|
|
||||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>({});
|
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||||
|
|
||||||
const update = useCallback((ev?: MatrixEvent) => {
|
const update = useCallback((ev?: MatrixEvent) => {
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
if (ev && ev.getType() !== EventType.RoomPowerLevels) return;
|
||||||
|
setPowerLevels(getPowerLevels(room));
|
||||||
const event = room.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
|
||||||
if (event) {
|
|
||||||
setPowerLevels(event.getContent());
|
|
||||||
} else {
|
|
||||||
setPowerLevels({});
|
|
||||||
}
|
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
useEventEmitter(cli, "RoomState.events", update);
|
useEventEmitter(cli, "RoomState.events", update);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,59 +14,60 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React, { ChangeEvent, createRef } from "react";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import EditableItemList from "../elements/EditableItemList";
|
import EditableItemList from "../elements/EditableItemList";
|
||||||
import React, {createRef} from 'react';
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import * as sdk from "../../../index";
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import RoomPublishSetting from "./RoomPublishSetting";
|
import RoomPublishSetting from "./RoomPublishSetting";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
|
|
||||||
class EditableAliasesList extends EditableItemList {
|
interface IEditableAliasesListProps {
|
||||||
constructor(props) {
|
domain?: string;
|
||||||
super(props);
|
}
|
||||||
|
|
||||||
this._aliasField = createRef();
|
class EditableAliasesList extends EditableItemList<IEditableAliasesListProps> {
|
||||||
}
|
private aliasField = createRef<RoomAliasField>();
|
||||||
|
|
||||||
_onAliasAdded = async () => {
|
private onAliasAdded = async () => {
|
||||||
await this._aliasField.current.validate({ allowEmpty: false });
|
await this.aliasField.current.validate({ allowEmpty: false });
|
||||||
|
|
||||||
if (this._aliasField.current.isValid) {
|
if (this.aliasField.current.isValid) {
|
||||||
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._aliasField.current.focus();
|
this.aliasField.current.focus();
|
||||||
this._aliasField.current.validate({ allowEmpty: false, focused: true });
|
this.aliasField.current.validate({ allowEmpty: false, focused: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderNewItemField() {
|
protected renderNewItemField() {
|
||||||
// if we don't need the RoomAliasField,
|
// if we don't need the RoomAliasField,
|
||||||
// we don't need to overriden version of _renderNewItemField
|
// we don't need to overriden version of renderNewItemField
|
||||||
if (!this.props.domain) {
|
if (!this.props.domain) {
|
||||||
return super._renderNewItemField();
|
return super.renderNewItemField();
|
||||||
}
|
}
|
||||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
const onChange = (alias) => this.onNewItemChanged({target: {value: alias}});
|
||||||
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={this._onAliasAdded}
|
onSubmit={this.onAliasAdded}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
noValidate={true}
|
noValidate={true}
|
||||||
className="mx_EditableItemList_newItem"
|
className="mx_EditableItemList_newItem"
|
||||||
>
|
>
|
||||||
<RoomAliasField
|
<RoomAliasField
|
||||||
ref={this._aliasField}
|
ref={this.aliasField}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={this.props.newItem || ""}
|
value={this.props.newItem || ""}
|
||||||
domain={this.props.domain} />
|
domain={this.props.domain} />
|
||||||
<AccessibleButton onClick={this._onAliasAdded} kind="primary">
|
<AccessibleButton onClick={this.onAliasAdded} kind="primary">
|
||||||
{ _t("Add") }
|
{ _t("Add") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</form>
|
</form>
|
||||||
|
@ -75,19 +75,30 @@ class EditableAliasesList extends EditableItemList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.room_settings.AliasSettings")
|
interface IProps {
|
||||||
export default class AliasSettings extends React.Component {
|
roomId: string;
|
||||||
static propTypes = {
|
canSetCanonicalAlias: boolean;
|
||||||
roomId: PropTypes.string.isRequired,
|
canSetAliases: boolean;
|
||||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
canonicalAliasEvent?: MatrixEvent;
|
||||||
canSetAliases: PropTypes.bool.isRequired,
|
hidePublishSetting?: boolean;
|
||||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
altAliases: string[];
|
||||||
|
localAliases: string[];
|
||||||
|
canonicalAlias?: string;
|
||||||
|
updatingCanonicalAlias: boolean;
|
||||||
|
localAliasesLoading: boolean;
|
||||||
|
detailsOpen: boolean;
|
||||||
|
newAlias?: string;
|
||||||
|
newAltAlias?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.room_settings.AliasSettings")
|
||||||
|
export default class AliasSettings extends React.Component<IProps, IState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
canSetAliases: false,
|
canSetAliases: false,
|
||||||
canSetCanonicalAlias: false,
|
canSetCanonicalAlias: false,
|
||||||
aliasEvents: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -122,7 +133,7 @@ export default class AliasSettings extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadLocalAliases() {
|
private async loadLocalAliases() {
|
||||||
this.setState({ localAliasesLoading: true });
|
this.setState({ localAliasesLoading: true });
|
||||||
try {
|
try {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -134,12 +145,16 @@ export default class AliasSettings extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({ localAliases });
|
this.setState({ localAliases });
|
||||||
|
|
||||||
|
if (localAliases.length === 0) {
|
||||||
|
this.setState({ detailsOpen: true });
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ localAliasesLoading: false });
|
this.setState({ localAliasesLoading: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeCanonicalAlias(alias) {
|
private changeCanonicalAlias(alias: string) {
|
||||||
if (!this.props.canSetCanonicalAlias) return;
|
if (!this.props.canSetCanonicalAlias) return;
|
||||||
|
|
||||||
const oldAlias = this.state.canonicalAlias;
|
const oldAlias = this.state.canonicalAlias;
|
||||||
|
@ -170,7 +185,7 @@ export default class AliasSettings extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeAltAliases(altAliases) {
|
private changeAltAliases(altAliases: string[]) {
|
||||||
if (!this.props.canSetCanonicalAlias) return;
|
if (!this.props.canSetCanonicalAlias) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -181,7 +196,7 @@ export default class AliasSettings extends React.Component {
|
||||||
const eventContent = {};
|
const eventContent = {};
|
||||||
|
|
||||||
if (this.state.canonicalAlias) {
|
if (this.state.canonicalAlias) {
|
||||||
eventContent.alias = this.state.canonicalAlias;
|
eventContent["alias"] = this.state.canonicalAlias;
|
||||||
}
|
}
|
||||||
if (altAliases) {
|
if (altAliases) {
|
||||||
eventContent["alt_aliases"] = altAliases;
|
eventContent["alt_aliases"] = altAliases;
|
||||||
|
@ -202,11 +217,11 @@ export default class AliasSettings extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onNewAliasChanged = (value) => {
|
private onNewAliasChanged = (value: string) => {
|
||||||
this.setState({newAlias: value});
|
this.setState({ newAlias: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
onLocalAliasAdded = (alias) => {
|
private onLocalAliasAdded = (alias: string) => {
|
||||||
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
if (!alias || alias.length === 0) return; // ignore attempts to create blank aliases
|
||||||
|
|
||||||
const localDomain = MatrixClientPeg.get().getDomain();
|
const localDomain = MatrixClientPeg.get().getDomain();
|
||||||
|
@ -232,7 +247,7 @@ export default class AliasSettings extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onLocalAliasDeleted = (index) => {
|
private onLocalAliasDeleted = (index: number) => {
|
||||||
const alias = this.state.localAliases[index];
|
const alias = this.state.localAliases[index];
|
||||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||||
// to this room. See https://github.com/vector-im/element-web/issues/7353
|
// to this room. See https://github.com/vector-im/element-web/issues/7353
|
||||||
|
@ -261,7 +276,7 @@ export default class AliasSettings extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onLocalAliasesToggled = (event) => {
|
private onLocalAliasesToggled = (event: ChangeEvent<HTMLDetailsElement>) => {
|
||||||
// expanded
|
// expanded
|
||||||
if (event.target.open) {
|
if (event.target.open) {
|
||||||
// if local aliases haven't been preloaded yet at component mount
|
// if local aliases haven't been preloaded yet at component mount
|
||||||
|
@ -269,43 +284,45 @@ export default class AliasSettings extends React.Component {
|
||||||
this.loadLocalAliases();
|
this.loadLocalAliases();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({detailsOpen: event.target.open});
|
this.setState({ detailsOpen: event.currentTarget.open });
|
||||||
};
|
};
|
||||||
|
|
||||||
onCanonicalAliasChange = (event) => {
|
private onCanonicalAliasChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||||
this.changeCanonicalAlias(event.target.value);
|
this.changeCanonicalAlias(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewAltAliasChanged = (value) => {
|
private onNewAltAliasChanged = (value: string) => {
|
||||||
this.setState({newAltAlias: value});
|
this.setState({ newAltAlias: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
onAltAliasAdded = (alias) => {
|
private onAltAliasAdded = (alias: string) => {
|
||||||
const altAliases = this.state.altAliases.slice();
|
const altAliases = this.state.altAliases.slice();
|
||||||
if (!altAliases.some(a => a.trim() === alias.trim())) {
|
if (!altAliases.some(a => a.trim() === alias.trim())) {
|
||||||
altAliases.push(alias.trim());
|
altAliases.push(alias.trim());
|
||||||
this.changeAltAliases(altAliases);
|
this.changeAltAliases(altAliases);
|
||||||
this.setState({newAltAlias: ""});
|
this.setState({ newAltAlias: "" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAltAliasDeleted = (index) => {
|
private onAltAliasDeleted = (index: number) => {
|
||||||
const altAliases = this.state.altAliases.slice();
|
const altAliases = this.state.altAliases.slice();
|
||||||
altAliases.splice(index, 1);
|
altAliases.splice(index, 1);
|
||||||
this.changeAltAliases(altAliases);
|
this.changeAltAliases(altAliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getAliases() {
|
private getAliases() {
|
||||||
return this.state.altAliases.concat(this._getLocalNonAltAliases());
|
return this.state.altAliases.concat(this.getLocalNonAltAliases());
|
||||||
}
|
}
|
||||||
|
|
||||||
_getLocalNonAltAliases() {
|
private getLocalNonAltAliases() {
|
||||||
const {altAliases} = this.state;
|
const {altAliases} = this.state;
|
||||||
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
|
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const localDomain = MatrixClientPeg.get().getDomain();
|
const cli = MatrixClientPeg.get();
|
||||||
|
const localDomain = cli.getDomain();
|
||||||
|
const isSpaceRoom = cli.getRoom(this.props.roomId)?.isSpaceRoom();
|
||||||
|
|
||||||
let found = false;
|
let found = false;
|
||||||
const canonicalValue = this.state.canonicalAlias || "";
|
const canonicalValue = this.state.canonicalAlias || "";
|
||||||
|
@ -320,7 +337,7 @@ export default class AliasSettings extends React.Component {
|
||||||
>
|
>
|
||||||
<option value="" key="unset">{ _t('not specified') }</option>
|
<option value="" key="unset">{ _t('not specified') }</option>
|
||||||
{
|
{
|
||||||
this._getAliases().map((alias, i) => {
|
this.getAliases().map((alias, i) => {
|
||||||
if (alias === this.state.canonicalAlias) found = true;
|
if (alias === this.state.canonicalAlias) found = true;
|
||||||
return (
|
return (
|
||||||
<option value={alias} key={i}>
|
<option value={alias} key={i}>
|
||||||
|
@ -340,12 +357,10 @@ export default class AliasSettings extends React.Component {
|
||||||
|
|
||||||
let localAliasesList;
|
let localAliasesList;
|
||||||
if (this.state.localAliasesLoading) {
|
if (this.state.localAliasesLoading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
localAliasesList = <Spinner />;
|
localAliasesList = <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
localAliasesList = (<EditableAliasesList
|
localAliasesList = (<EditableAliasesList
|
||||||
id="roomAliases"
|
id="roomAliases"
|
||||||
className={"mx_RoomSettings_localAliases"}
|
|
||||||
items={this.state.localAliases}
|
items={this.state.localAliases}
|
||||||
newItem={this.state.newAlias}
|
newItem={this.state.newAlias}
|
||||||
onNewItemChanged={this.onNewAliasChanged}
|
onNewItemChanged={this.onNewAliasChanged}
|
||||||
|
@ -353,7 +368,9 @@ export default class AliasSettings extends React.Component {
|
||||||
canEdit={this.props.canSetAliases}
|
canEdit={this.props.canSetAliases}
|
||||||
onItemAdded={this.onLocalAliasAdded}
|
onItemAdded={this.onLocalAliasAdded}
|
||||||
onItemRemoved={this.onLocalAliasDeleted}
|
onItemRemoved={this.onLocalAliasDeleted}
|
||||||
noItemsLabel={_t('This room has no local addresses')}
|
noItemsLabel={isSpaceRoom
|
||||||
|
? _t("This space has no local addresses")
|
||||||
|
: _t("This room has no local addresses")}
|
||||||
placeholder={_t('Local address')}
|
placeholder={_t('Local address')}
|
||||||
domain={localDomain}
|
domain={localDomain}
|
||||||
/>);
|
/>);
|
||||||
|
@ -362,18 +379,27 @@ export default class AliasSettings extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className='mx_AliasSettings'>
|
<div className='mx_AliasSettings'>
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
|
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
|
||||||
<p>{_t("Published addresses can be used by anyone on any server to join your room. " +
|
<p>
|
||||||
"To publish an address, it needs to be set as a local address first.")}</p>
|
{ isSpaceRoom
|
||||||
{canonicalAliasSection}
|
? _t("Published addresses can be used by anyone on any server to join your space.")
|
||||||
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
|
: _t("Published addresses can be used by anyone on any server to join your room.")}
|
||||||
|
|
||||||
|
{ _t("To publish an address, it needs to be set as a local address first.") }
|
||||||
|
</p>
|
||||||
|
{ canonicalAliasSection }
|
||||||
|
{ this.props.hidePublishSetting
|
||||||
|
? null
|
||||||
|
: <RoomPublishSetting
|
||||||
|
roomId={this.props.roomId}
|
||||||
|
canSetCanonicalAlias={this.props.canSetCanonicalAlias}
|
||||||
|
/> }
|
||||||
<datalist id="mx_AliasSettings_altRecommendations">
|
<datalist id="mx_AliasSettings_altRecommendations">
|
||||||
{this._getLocalNonAltAliases().map(alias => {
|
{this.getLocalNonAltAliases().map(alias => {
|
||||||
return <option value={alias} key={alias} />;
|
return <option value={alias} key={alias} />;
|
||||||
})};
|
})};
|
||||||
</datalist>
|
</datalist>
|
||||||
<EditableAliasesList
|
<EditableAliasesList
|
||||||
id="roomAltAliases"
|
id="roomAltAliases"
|
||||||
className={"mx_RoomSettings_altAliases"}
|
|
||||||
items={this.state.altAliases}
|
items={this.state.altAliases}
|
||||||
newItem={this.state.newAltAlias}
|
newItem={this.state.newAltAlias}
|
||||||
onNewItemChanged={this.onNewAltAliasChanged}
|
onNewItemChanged={this.onNewAltAliasChanged}
|
||||||
|
@ -386,11 +412,19 @@ export default class AliasSettings extends React.Component {
|
||||||
noItemsLabel={_t('No other published addresses yet, add one below')}
|
noItemsLabel={_t('No other published addresses yet, add one below')}
|
||||||
placeholder={_t('New published address (e.g. #alias:server)')}
|
placeholder={_t('New published address (e.g. #alias:server)')}
|
||||||
/>
|
/>
|
||||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
|
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>
|
||||||
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
|
{ _t("Local Addresses") }
|
||||||
<details onToggle={this.onLocalAliasesToggled}>
|
</span>
|
||||||
|
<p>
|
||||||
|
{ isSpaceRoom
|
||||||
|
? _t("Set addresses for this space so users can find this space " +
|
||||||
|
"through your homeserver (%(localDomain)s)", { localDomain })
|
||||||
|
: _t("Set addresses for this room so users can find this room " +
|
||||||
|
"through your homeserver (%(localDomain)s)", { localDomain }) }
|
||||||
|
</p>
|
||||||
|
<details onToggle={this.onLocalAliasesToggled} open={this.state.detailsOpen}>
|
||||||
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
|
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
|
||||||
{localAliasesList}
|
{ localAliasesList }
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,20 +14,34 @@ 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 LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
label?: string;
|
||||||
|
canSetCanonicalAlias?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isRoomPublished: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.room_settings.RoomPublishSetting")
|
@replaceableComponent("views.room_settings.RoomPublishSetting")
|
||||||
export default class RoomPublishSetting extends React.PureComponent {
|
export default class RoomPublishSetting extends React.PureComponent<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props, context);
|
||||||
this.state = {isRoomPublished: false};
|
|
||||||
|
this.state = {
|
||||||
|
isRoomPublished: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onRoomPublishChange = (e) => {
|
private onRoomPublishChange = (e) => {
|
||||||
const valueBefore = this.state.isRoomPublished;
|
const valueBefore = this.state.isRoomPublished;
|
||||||
const newValue = !valueBefore;
|
const newValue = !valueBefore;
|
||||||
this.setState({isRoomPublished: newValue});
|
this.setState({isRoomPublished: newValue});
|
||||||
|
@ -52,11 +66,14 @@ export default class RoomPublishSetting extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
||||||
return (<LabelledToggleSwitch value={this.state.isRoomPublished}
|
return (
|
||||||
onChange={this.onRoomPublishChange}
|
<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||||
disabled={!this.props.canSetCanonicalAlias}
|
onChange={this.onRoomPublishChange}
|
||||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
disabled={!this.props.canSetCanonicalAlias}
|
||||||
domain: client.getDomain(),
|
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||||
})} />);
|
domain: client.getDomain(),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017, 2018 New Vector Ltd
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,17 +21,28 @@ import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||||
import rate_limited_func from "../../../ratelimitedfunc";
|
import rateLimitedFunction from "../../../ratelimitedfunc";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../index";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
|
||||||
import BaseCard from "../right_panel/BaseCard";
|
import BaseCard from "../right_panel/BaseCard";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
|
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
import TruncatedList from '../elements/TruncatedList';
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
import SearchBox from "../../structures/SearchBox";
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
import EntityTile from "./EntityTile";
|
||||||
|
import MemberTile from "./MemberTile";
|
||||||
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||||
|
@ -40,41 +52,59 @@ const SHOW_MORE_INCREMENT = 100;
|
||||||
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
||||||
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
loading: boolean;
|
||||||
|
members: Array<RoomMember>;
|
||||||
|
filteredJoinedMembers: Array<RoomMember>;
|
||||||
|
filteredInvitedMembers: Array<RoomMember | MatrixEvent>;
|
||||||
|
canInvite: boolean;
|
||||||
|
truncateAtJoined: number;
|
||||||
|
truncateAtInvited: number;
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.MemberList")
|
@replaceableComponent("views.rooms.MemberList")
|
||||||
export default class MemberList extends React.Component {
|
export default class MemberList extends React.Component<IProps, IState> {
|
||||||
|
private showPresence = true;
|
||||||
|
private mounted = false;
|
||||||
|
private collator: Intl.Collator;
|
||||||
|
private sortNames = new Map<RoomMember, string>(); // RoomMember -> sortName
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli.hasLazyLoadMembersEnabled()) {
|
if (cli.hasLazyLoadMembersEnabled()) {
|
||||||
// show an empty list
|
// show an empty list
|
||||||
this.state = this._getMembersState([]);
|
this.state = this.getMembersState([]);
|
||||||
} else {
|
} else {
|
||||||
this.state = this._getMembersState(this.roomMembers());
|
this.state = this.getMembersState(this.roomMembers());
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.on("Room", this.onRoom); // invites & joining after peek
|
cli.on("Room", this.onRoom); // invites & joining after peek
|
||||||
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
|
||||||
const hsUrl = MatrixClientPeg.get().baseUrl;
|
const hsUrl = MatrixClientPeg.get().baseUrl;
|
||||||
this._showPresence = true;
|
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
|
||||||
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
|
|
||||||
this._showPresence = enablePresenceByHsUrl[hsUrl];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
this._mounted = true;
|
this.mounted = true;
|
||||||
if (cli.hasLazyLoadMembersEnabled()) {
|
if (cli.hasLazyLoadMembersEnabled()) {
|
||||||
this._showMembersAccordingToMembershipWithLL();
|
this.showMembersAccordingToMembershipWithLL();
|
||||||
cli.on("Room.myMembership", this.onMyMembership);
|
cli.on("Room.myMembership", this.onMyMembership);
|
||||||
} else {
|
} else {
|
||||||
this._listenForMembersChanges();
|
this.listenForMembersChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_listenForMembersChanges() {
|
private listenForMembersChanges(): void {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.members", this.onRoomStateMember);
|
cli.on("RoomState.members", this.onRoomStateMember);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
@ -89,7 +119,7 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._mounted = false;
|
this.mounted = false;
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli) {
|
||||||
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
cli.removeListener("RoomState.members", this.onRoomStateMember);
|
||||||
|
@ -103,7 +133,7 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this._updateList.cancelPendingCall();
|
this.updateList.cancelPendingCall();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -111,7 +141,7 @@ export default class MemberList extends React.Component {
|
||||||
* show a spinner and load the members if the user is joined,
|
* show a spinner and load the members if the user is joined,
|
||||||
* or show the members available so far if the user is invited
|
* or show the members available so far if the user is invited
|
||||||
*/
|
*/
|
||||||
async _showMembersAccordingToMembershipWithLL() {
|
private async showMembersAccordingToMembershipWithLL(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli.hasLazyLoadMembersEnabled()) {
|
if (cli.hasLazyLoadMembersEnabled()) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -122,31 +152,31 @@ export default class MemberList extends React.Component {
|
||||||
try {
|
try {
|
||||||
await room.loadMembersIfNeeded();
|
await room.loadMembersIfNeeded();
|
||||||
} catch (ex) {/* already logged in RoomView */}
|
} catch (ex) {/* already logged in RoomView */}
|
||||||
if (this._mounted) {
|
if (this.mounted) {
|
||||||
this.setState(this._getMembersState(this.roomMembers()));
|
this.setState(this.getMembersState(this.roomMembers()));
|
||||||
this._listenForMembersChanges();
|
this.listenForMembersChanges();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// show the members we already have loaded
|
// show the members we already have loaded
|
||||||
this.setState(this._getMembersState(this.roomMembers()));
|
this.setState(this.getMembersState(this.roomMembers()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get canInvite() {
|
private get canInvite(): boolean {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
return room && room.canInvite(cli.getUserId());
|
return room && room.canInvite(cli.getUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMembersState(members) {
|
private getMembersState(members: Array<RoomMember>): IState {
|
||||||
// set the state after determining _showPresence to make sure it's
|
// set the state after determining showPresence to make sure it's
|
||||||
// taken into account while rerendering
|
// taken into account while rendering
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
members: members,
|
members: members,
|
||||||
filteredJoinedMembers: this._filterMembers(members, 'join'),
|
filteredJoinedMembers: this.filterMembers(members, 'join'),
|
||||||
filteredInvitedMembers: this._filterMembers(members, 'invite'),
|
filteredInvitedMembers: this.filterMembers(members, 'invite'),
|
||||||
canInvite: this.canInvite,
|
canInvite: this.canInvite,
|
||||||
|
|
||||||
// ideally we'd size this to the page height, but
|
// ideally we'd size this to the page height, but
|
||||||
|
@ -157,72 +187,72 @@ export default class MemberList extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onUserPresenceChange = (event, user) => {
|
private onUserPresenceChange = (event: MatrixEvent, user: User): void => {
|
||||||
// Attach a SINGLE listener for global presence changes then locate the
|
// Attach a SINGLE listener for global presence changes then locate the
|
||||||
// member tile and re-render it. This is more efficient than every tile
|
// member tile and re-render it. This is more efficient than every tile
|
||||||
// ever attaching their own listener.
|
// ever attaching their own listener.
|
||||||
const tile = this.refs[user.userId];
|
const tile = this.refs[user.userId];
|
||||||
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
// console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
this._updateList(); // reorder the membership list
|
this.updateList(); // reorder the membership list
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoom = room => {
|
private onRoom = (room: Room): void => {
|
||||||
if (room.roomId !== this.props.roomId) {
|
if (room.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// We listen for room events because when we accept an invite
|
// We listen for room events because when we accept an invite
|
||||||
// we need to wait till the room is fully populated with state
|
// we need to wait till the room is fully populated with state
|
||||||
// before refreshing the member list else we get a stale list.
|
// before refreshing the member list else we get a stale list.
|
||||||
this._showMembersAccordingToMembershipWithLL();
|
this.showMembersAccordingToMembershipWithLL();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMyMembership = (room, membership, oldMembership) => {
|
private onMyMembership = (room: Room, membership: string, oldMembership: string): void => {
|
||||||
if (room.roomId === this.props.roomId && membership === "join") {
|
if (room.roomId === this.props.roomId && membership === "join") {
|
||||||
this._showMembersAccordingToMembershipWithLL();
|
this.showMembersAccordingToMembershipWithLL();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomStateMember = (ev, state, member) => {
|
private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => {
|
||||||
if (member.roomId !== this.props.roomId) {
|
if (member.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._updateList();
|
this.updateList();
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomMemberName = (ev, member) => {
|
private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => {
|
||||||
if (member.roomId !== this.props.roomId) {
|
if (member.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._updateList();
|
this.updateList();
|
||||||
};
|
};
|
||||||
|
|
||||||
onRoomStateEvent = (event, state) => {
|
private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => {
|
||||||
if (event.getRoomId() === this.props.roomId &&
|
if (event.getRoomId() === this.props.roomId &&
|
||||||
event.getType() === "m.room.third_party_invite") {
|
event.getType() === "m.room.third_party_invite") {
|
||||||
this._updateList();
|
this.updateList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateList = rate_limited_func(() => {
|
private updateList = rateLimitedFunction(() => {
|
||||||
this._updateListNow();
|
this.updateListNow();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
_updateListNow() {
|
private updateListNow(): void {
|
||||||
// console.log("Updating memberlist");
|
const members = this.roomMembers()
|
||||||
const newState = {
|
|
||||||
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
members: this.roomMembers(),
|
members: members,
|
||||||
};
|
filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery),
|
||||||
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
|
filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery),
|
||||||
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
|
});
|
||||||
this.setState(newState);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getMembersWithUser() {
|
private getMembersWithUser(): Array<RoomMember> {
|
||||||
if (!this.props.roomId) return [];
|
if (!this.props.roomId) return [];
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
|
@ -230,15 +260,18 @@ export default class MemberList extends React.Component {
|
||||||
|
|
||||||
const allMembers = Object.values(room.currentState.members);
|
const allMembers = Object.values(room.currentState.members);
|
||||||
|
|
||||||
allMembers.forEach(function(member) {
|
allMembers.forEach((member) => {
|
||||||
// work around a race where you might have a room member object
|
// work around a race where you might have a room member object
|
||||||
// before the user object exists. This may or may not cause
|
// before the user object exists. This may or may not cause
|
||||||
// https://github.com/vector-im/vector-web/issues/186
|
// https://github.com/vector-im/vector-web/issues/186
|
||||||
if (member.user === null) {
|
if (!member.user) {
|
||||||
member.user = cli.getUser(member.userId);
|
member.user = cli.getUser(member.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, "");
|
this.sortNames.set(
|
||||||
|
member,
|
||||||
|
(member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""),
|
||||||
|
);
|
||||||
|
|
||||||
// XXX: this user may have no lastPresenceTs value!
|
// XXX: this user may have no lastPresenceTs value!
|
||||||
// the right solution here is to fix the race rather than leave it as 0
|
// the right solution here is to fix the race rather than leave it as 0
|
||||||
|
@ -247,7 +280,7 @@ export default class MemberList extends React.Component {
|
||||||
return allMembers;
|
return allMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
roomMembers() {
|
private roomMembers(): Array<RoomMember> {
|
||||||
const allMembers = this.getMembersWithUser();
|
const allMembers = this.getMembersWithUser();
|
||||||
const filteredAndSortedMembers = allMembers.filter((m) => {
|
const filteredAndSortedMembers = allMembers.filter((m) => {
|
||||||
return (
|
return (
|
||||||
|
@ -255,23 +288,21 @@ export default class MemberList extends React.Component {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const language = SettingsStore.getValue("language");
|
const language = SettingsStore.getValue("language");
|
||||||
this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true });
|
this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false });
|
||||||
filteredAndSortedMembers.sort(this.memberSort);
|
filteredAndSortedMembers.sort(this.memberSort);
|
||||||
return filteredAndSortedMembers;
|
return filteredAndSortedMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createOverflowTileJoined = (overflowCount, totalCount) => {
|
private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
|
return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList);
|
||||||
};
|
};
|
||||||
|
|
||||||
_createOverflowTileInvited = (overflowCount, totalCount) => {
|
private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => {
|
||||||
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
|
return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList);
|
||||||
};
|
};
|
||||||
|
|
||||||
_createOverflowTile = (overflowCount, totalCount, onClick) => {
|
private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> {
|
||||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
|
||||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
|
||||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
return (
|
return (
|
||||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
@ -281,31 +312,48 @@ export default class MemberList extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreJoinedMemberList = () => {
|
private showMoreJoinedMemberList = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreInvitedMemberList = () => {
|
private showMoreInvitedMemberList = (): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
memberString(member) {
|
/**
|
||||||
|
* SHOULD ONLY BE USED BY TESTS
|
||||||
|
*/
|
||||||
|
public memberString(member: RoomMember): string {
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return "(null)";
|
return "(null)";
|
||||||
} else {
|
} else {
|
||||||
const u = member.user;
|
const u = member.user;
|
||||||
return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")";
|
return (
|
||||||
|
"(" +
|
||||||
|
member.name +
|
||||||
|
", " +
|
||||||
|
member.powerLevel +
|
||||||
|
", " +
|
||||||
|
(u ? u.lastActiveAgo : "<null>") +
|
||||||
|
", " +
|
||||||
|
(u ? u.getLastActiveTs() : "<null>") +
|
||||||
|
", " +
|
||||||
|
(u ? u.currentlyActive : "<null>") +
|
||||||
|
", " +
|
||||||
|
(u ? u.presence : "<null>") +
|
||||||
|
")"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns negative if a comes before b,
|
// returns negative if a comes before b,
|
||||||
// returns 0 if a and b are equivalent in ordering
|
// returns 0 if a and b are equivalent in ordering
|
||||||
// returns positive if a comes after b.
|
// returns positive if a comes after b.
|
||||||
memberSort = (memberA, memberB) => {
|
private memberSort = (memberA: RoomMember, memberB: RoomMember): number => {
|
||||||
// order by presence, with "active now" first.
|
// order by presence, with "active now" first.
|
||||||
// ...and then by power level
|
// ...and then by power level
|
||||||
// ...and then by last active
|
// ...and then by last active
|
||||||
|
@ -325,7 +373,7 @@ export default class MemberList extends React.Component {
|
||||||
if (!userA && userB) return 1;
|
if (!userA && userB) return 1;
|
||||||
|
|
||||||
// First by presence
|
// First by presence
|
||||||
if (this._showPresence) {
|
if (this.showPresence) {
|
||||||
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
const convertPresence = (p) => p === 'unavailable' ? 'online' : p;
|
||||||
const presenceIndex = p => {
|
const presenceIndex = p => {
|
||||||
const order = ['active', 'online', 'offline'];
|
const order = ['active', 'online', 'offline'];
|
||||||
|
@ -349,31 +397,31 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third by last active
|
// Third by last active
|
||||||
if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) {
|
||||||
// console.log("Comparing on last active timestamp - returning");
|
// console.log("Comparing on last active timestamp - returning");
|
||||||
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
return userB.getLastActiveTs() - userA.getLastActiveTs();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fourth by name (alphabetical)
|
// Fourth by name (alphabetical)
|
||||||
return this.collator.compare(memberA.sortName, memberB.sortName);
|
return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB));
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchQueryChanged = searchQuery => {
|
private onSearchQueryChanged = (searchQuery: string): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchQuery,
|
searchQuery,
|
||||||
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
|
filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery),
|
||||||
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
|
filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPending3pidInviteClick = inviteEvent => {
|
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_3pid_invite',
|
action: 'view_3pid_invite',
|
||||||
event: inviteEvent,
|
event: inviteEvent,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_filterMembers(members, membership, query) {
|
private filterMembers(members: Array<RoomMember>, membership: string, query?: string): Array<RoomMember> {
|
||||||
return members.filter((m) => {
|
return members.filter((m) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
query = query.toLowerCase();
|
query = query.toLowerCase();
|
||||||
|
@ -389,7 +437,7 @@ export default class MemberList extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getPending3PidInvites() {
|
private getPending3PidInvites(): Array<MatrixEvent> {
|
||||||
// include 3pid invites (m.room.third_party_invite) state events.
|
// include 3pid invites (m.room.third_party_invite) state events.
|
||||||
// The HS may have already converted these into m.room.member invites so
|
// The HS may have already converted these into m.room.member invites so
|
||||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||||
|
@ -409,42 +457,40 @@ export default class MemberList extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_makeMemberTiles(members) {
|
private makeMemberTiles(members: Array<RoomMember | MatrixEvent>) {
|
||||||
const MemberTile = sdk.getComponent("rooms.MemberTile");
|
|
||||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
|
||||||
|
|
||||||
return members.map((m) => {
|
return members.map((m) => {
|
||||||
if (m.userId) {
|
if (m instanceof RoomMember) {
|
||||||
// Is a Matrix invite
|
// Is a Matrix invite
|
||||||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />;
|
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
||||||
} else {
|
} else {
|
||||||
// Is a 3pid invite
|
// Is a 3pid invite
|
||||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||||
onClick={() => this._onPending3pidInviteClick(m)} />;
|
onClick={() => this.onPending3pidInviteClick(m)} />;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
|
private getChildrenJoined = (start: number, end: number): Array<JSX.Element> => {
|
||||||
|
return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end))
|
||||||
_getChildCountJoined = () => this.state.filteredJoinedMembers.length;
|
|
||||||
|
|
||||||
_getChildrenInvited = (start, end) => {
|
|
||||||
let targets = this.state.filteredInvitedMembers;
|
|
||||||
if (end > this.state.filteredInvitedMembers.length) {
|
|
||||||
targets = targets.concat(this._getPending3PidInvites());
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._makeMemberTiles(targets.slice(start, end));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_getChildCountInvited = () => {
|
private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length;
|
||||||
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
|
|
||||||
|
private getChildrenInvited = (start: number, end: number): Array<JSX.Element> => {
|
||||||
|
let targets = this.state.filteredInvitedMembers;
|
||||||
|
if (end > this.state.filteredInvitedMembers.length) {
|
||||||
|
targets = targets.concat(this.getPending3PidInvites());
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.makeMemberTiles(targets.slice(start, end));
|
||||||
|
};
|
||||||
|
|
||||||
|
private getChildCountInvited = (): number => {
|
||||||
|
return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
const Spinner = sdk.getComponent("elements.Spinner");
|
|
||||||
return <BaseCard
|
return <BaseCard
|
||||||
className="mx_MemberList"
|
className="mx_MemberList"
|
||||||
onClose={this.props.onClose}
|
onClose={this.props.onClose}
|
||||||
|
@ -454,9 +500,6 @@ export default class MemberList extends React.Component {
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchBox = sdk.getComponent('structures.SearchBox');
|
|
||||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
let inviteButton;
|
let inviteButton;
|
||||||
|
@ -470,22 +513,30 @@ export default class MemberList extends React.Component {
|
||||||
inviteButtonText = _t("Invite to this space");
|
inviteButtonText = _t("Invite to this space");
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
inviteButton = (
|
||||||
inviteButton =
|
<AccessibleButton
|
||||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!this.state.canInvite}>
|
className="mx_MemberList_invite"
|
||||||
|
onClick={this.onInviteButtonClick}
|
||||||
|
disabled={!this.state.canInvite}
|
||||||
|
>
|
||||||
<span>{ inviteButtonText }</span>
|
<span>{ inviteButtonText }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let invitedHeader;
|
let invitedHeader;
|
||||||
let invitedSection;
|
let invitedSection;
|
||||||
if (this._getChildCountInvited() > 0) {
|
if (this.getChildCountInvited() > 0) {
|
||||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||||
invitedSection = <TruncatedList className="mx_MemberList_section mx_MemberList_invited" truncateAt={this.state.truncateAtInvited}
|
invitedSection = (
|
||||||
createOverflowElement={this._createOverflowTileInvited}
|
<TruncatedList
|
||||||
getChildren={this._getChildrenInvited}
|
className="mx_MemberList_section mx_MemberList_invited"
|
||||||
getChildCount={this._getChildCountInvited}
|
truncateAt={this.state.truncateAtInvited}
|
||||||
/>;
|
createOverflowElement={this.createOverflowTileInvited}
|
||||||
|
getChildren={this.getChildrenInvited}
|
||||||
|
getChildCount={this.getChildCountInvited}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
|
@ -517,17 +568,19 @@ export default class MemberList extends React.Component {
|
||||||
previousPhase={previousPhase}
|
previousPhase={previousPhase}
|
||||||
>
|
>
|
||||||
<div className="mx_MemberList_wrapper">
|
<div className="mx_MemberList_wrapper">
|
||||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
<TruncatedList
|
||||||
createOverflowElement={this._createOverflowTileJoined}
|
className="mx_MemberList_section mx_MemberList_joined"
|
||||||
getChildren={this._getChildrenJoined}
|
truncateAt={this.state.truncateAtJoined}
|
||||||
getChildCount={this._getChildCountJoined} />
|
createOverflowElement={this.createOverflowTileJoined}
|
||||||
|
getChildren={this.getChildrenJoined}
|
||||||
|
getChildCount={this.getChildCountJoined} />
|
||||||
{ invitedHeader }
|
{ invitedHeader }
|
||||||
{ invitedSection }
|
{ invitedSection }
|
||||||
</div>
|
</div>
|
||||||
</BaseCard>;
|
</BaseCard>;
|
||||||
}
|
}
|
||||||
|
|
||||||
onInviteButtonClick = () => {
|
onInviteButtonClick = (): void => {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'require_registration'});
|
dis.dispatch({action: 'require_registration'});
|
||||||
return;
|
return;
|
|
@ -466,6 +466,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
|
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
|
||||||
|
if (SettingsStore.getValue("feature_spaces")) return [];
|
||||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||||
// See https://github.com/vector-im/element-web/issues/14456
|
// See https://github.com/vector-im/element-web/issues/14456
|
||||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||||
|
|
|
@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||||
import {MsgType} from "matrix-js-sdk/src/@types/event";
|
import {MsgType} from "matrix-js-sdk/src/@types/event";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
import CallMediaHandler from "../../../CallMediaHandler";
|
import MediaDeviceHandler from "../../../MediaDeviceHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -129,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
// Do a sanity test to ensure we're about to grab a valid microphone reference. Things might
|
// Do a sanity test to ensure we're about to grab a valid microphone reference. Things might
|
||||||
// change between this and recording, but at least we will have tried.
|
// change between this and recording, but at least we will have tried.
|
||||||
try {
|
try {
|
||||||
const devices = await CallMediaHandler.getDevices();
|
const devices = await MediaDeviceHandler.getDevices();
|
||||||
if (!devices?.['audioinput']?.length) {
|
if (!devices?.['audioInput']?.length) {
|
||||||
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
|
||||||
title: _t("No microphone found"),
|
title: _t("No microphone found"),
|
||||||
description: <>
|
description: <>
|
||||||
|
|
|
@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
async _getUpdatedStatus() {
|
async _getUpdatedStatus() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const pkCache = cli.getCrossSigningCacheCallbacks();
|
const pkCache = cli.getCrossSigningCacheCallbacks();
|
||||||
const crossSigning = cli.crypto._crossSigningInfo;
|
const crossSigning = cli.crypto.crossSigningInfo;
|
||||||
const secretStorage = cli.crypto._secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
const crossSigningPublicKeysOnDevice = crossSigning.getId();
|
||||||
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage);
|
||||||
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master"));
|
||||||
|
|
|
@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent {
|
||||||
|
|
||||||
async _getUpdatedDiagnostics() {
|
async _getUpdatedDiagnostics() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const secretStorage = cli.crypto._secretStorage;
|
const secretStorage = cli.crypto.secretStorage;
|
||||||
|
|
||||||
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
const backupKeyStored = !!(await cli.isKeyBackupKeyStored());
|
||||||
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,68 +15,76 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
import {_t} from "../../../../../languageHandler";
|
|
||||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import * as sdk from "../../../../..";
|
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
import RoomUpgradeDialog from "../../../dialogs/RoomUpgradeDialog";
|
||||||
|
import DevtoolsDialog from "../../../dialogs/DevtoolsDialog";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
roomId: string;
|
||||||
|
closeSettingsFn(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRecommendedVersion {
|
||||||
|
version: string;
|
||||||
|
needsUpgrade: boolean;
|
||||||
|
urgent: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
upgradeRecommendation?: IRecommendedVersion;
|
||||||
|
oldRoomId?: string;
|
||||||
|
oldEventId?: string;
|
||||||
|
upgraded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
|
@replaceableComponent("views.settings.tabs.room.AdvancedRoomSettingsTab")
|
||||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
export default class AdvancedRoomSettingsTab extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props, context) {
|
||||||
roomId: PropTypes.string.isRequired,
|
super(props, context);
|
||||||
closeSettingsFn: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Move this to constructor
|
|
||||||
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
|
||||||
// we handle lack of this object gracefully later, so don't worry about it failing here.
|
// we handle lack of this object gracefully later, so don't worry about it failing here.
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
room.getRecommendedVersion().then((v) => {
|
room.getRecommendedVersion().then((v) => {
|
||||||
const tombstone = room.currentState.getStateEvents("m.room.tombstone", "");
|
const tombstone = room.currentState.getStateEvents(EventType.RoomTombstone, "");
|
||||||
|
|
||||||
const additionalStateChanges = {};
|
const additionalStateChanges: Partial<IState> = {};
|
||||||
const createEvent = room.currentState.getStateEvents("m.room.create", "");
|
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||||
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
|
const predecessor = createEvent ? createEvent.getContent().predecessor : null;
|
||||||
if (predecessor && predecessor.room_id) {
|
if (predecessor && predecessor.room_id) {
|
||||||
additionalStateChanges['oldRoomId'] = predecessor.room_id;
|
additionalStateChanges.oldRoomId = predecessor.room_id;
|
||||||
additionalStateChanges['oldEventId'] = predecessor.event_id;
|
additionalStateChanges.oldEventId = predecessor.event_id;
|
||||||
additionalStateChanges['hasPreviousRoom'] = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
upgraded: tombstone && tombstone.getContent().replacement_room,
|
upgraded: !!tombstone?.getContent().replacement_room,
|
||||||
upgradeRecommendation: v,
|
upgradeRecommendation: v,
|
||||||
...additionalStateChanges,
|
...additionalStateChanges,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_upgradeRoom = (e) => {
|
private upgradeRoom = (e) => {
|
||||||
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: room});
|
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room });
|
||||||
};
|
};
|
||||||
|
|
||||||
_openDevtools = (e) => {
|
private openDevtools = (e) => {
|
||||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
|
||||||
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
|
Modal.createDialog(DevtoolsDialog, {roomId: this.props.roomId});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onOldRoomClicked = (e) => {
|
private onOldRoomClicked = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -93,9 +101,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
|
|
||||||
let unfederatableSection;
|
let unfederatableSection;
|
||||||
const createEvent = room.currentState.getStateEvents('m.room.create', '');
|
const createEvent = room.currentState.getStateEvents(EventType.RoomCreate, '');
|
||||||
if (createEvent && createEvent.getContent()['m.federate'] === false) {
|
if (createEvent && createEvent.getContent()['m.federate'] === false) {
|
||||||
unfederatableSection = <div>{_t('This room is not accessible by remote Matrix servers')}</div>;
|
unfederatableSection = <div>{ _t('This room is not accessible by remote Matrix servers') }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomUpgradeButton;
|
let roomUpgradeButton;
|
||||||
|
@ -103,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
roomUpgradeButton = (
|
roomUpgradeButton = (
|
||||||
<div>
|
<div>
|
||||||
<p className='mx_SettingsTab_warningText'>
|
<p className='mx_SettingsTab_warningText'>
|
||||||
{_t(
|
{ _t(
|
||||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
|
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
|
||||||
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
||||||
"version of the room - room members will have to click this link to join the new room.",
|
"version of the room - room members will have to click this link to join the new room.",
|
||||||
|
@ -111,51 +119,53 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
"b": (sub) => <b>{sub}</b>,
|
"b": (sub) => <b>{sub}</b>,
|
||||||
"i": (sub) => <i>{sub}</i>,
|
"i": (sub) => <i>{sub}</i>,
|
||||||
},
|
},
|
||||||
)}
|
) }
|
||||||
</p>
|
</p>
|
||||||
<AccessibleButton onClick={this._upgradeRoom} kind='primary'>
|
<AccessibleButton onClick={this.upgradeRoom} kind='primary'>
|
||||||
{_t("Upgrade this room to the recommended room version")}
|
{ _t("Upgrade this room to the recommended room version") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldRoomLink;
|
let oldRoomLink;
|
||||||
if (this.state.hasPreviousRoom) {
|
if (this.state.oldRoomId) {
|
||||||
let name = _t("this room");
|
let name = _t("this room");
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
if (room && room.name) name = room.name;
|
if (room && room.name) name = room.name;
|
||||||
oldRoomLink = (
|
oldRoomLink = (
|
||||||
<AccessibleButton element='a' onClick={this._onOldRoomClicked}>
|
<AccessibleButton element='a' onClick={this.onOldRoomClicked}>
|
||||||
{_t("View older messages in %(roomName)s.", {roomName: name})}
|
{ _t("View older messages in %(roomName)s.", { roomName: name }) }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab">
|
<div className="mx_SettingsTab">
|
||||||
<div className="mx_SettingsTab_heading">{_t("Advanced")}</div>
|
<div className="mx_SettingsTab_heading">{ _t("Advanced") }</div>
|
||||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Room information")}</span>
|
<span className='mx_SettingsTab_subheading'>
|
||||||
|
{ room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
|
||||||
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{_t("Internal room ID:")}</span>
|
<span>{ _t("Internal room ID:") }</span>
|
||||||
{this.props.roomId}
|
{ this.props.roomId }
|
||||||
</div>
|
</div>
|
||||||
{unfederatableSection}
|
{ unfederatableSection }
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Room version")}</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Room version") }</span>
|
||||||
<div>
|
<div>
|
||||||
<span>{_t("Room version:")}</span>
|
<span>{ _t("Room version:") }</span>
|
||||||
{room.getVersion()}
|
{ room.getVersion() }
|
||||||
</div>
|
</div>
|
||||||
{oldRoomLink}
|
{ oldRoomLink }
|
||||||
{roomUpgradeButton}
|
{ roomUpgradeButton }
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
|
<span className='mx_SettingsTab_subheading'>{ _t("Developer options") }</span>
|
||||||
<AccessibleButton onClick={this._openDevtools} kind='primary'>
|
<AccessibleButton onClick={this.openDevtools} kind='primary'>
|
||||||
{_t("Open Devtools")}
|
{ _t("Open Devtools") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -60,7 +60,6 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||||
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this
|
||||||
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
||||||
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
|
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
|
||||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases");
|
|
||||||
|
|
||||||
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
|
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
|
||||||
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
|
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
|
||||||
|
@ -100,7 +99,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
|
||||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||||
<AliasSettings roomId={this.props.roomId}
|
<AliasSettings roomId={this.props.roomId}
|
||||||
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases}
|
||||||
canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
|
canonicalAliasEvent={canonicalAliasEv} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
<div className="mx_SettingsTab_heading">{_t("Other")}</div>
|
||||||
{ flairSection }
|
{ flairSection }
|
||||||
|
|
|
@ -29,19 +29,19 @@ import {UIFeature} from "../../../../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// Knock and private are reserved keywords which are not yet implemented.
|
// Knock and private are reserved keywords which are not yet implemented.
|
||||||
enum JoinRule {
|
export enum JoinRule {
|
||||||
Public = "public",
|
Public = "public",
|
||||||
Knock = "knock",
|
Knock = "knock",
|
||||||
Invite = "invite",
|
Invite = "invite",
|
||||||
Private = "private",
|
Private = "private",
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GuestAccess {
|
export enum GuestAccess {
|
||||||
CanJoin = "can_join",
|
CanJoin = "can_join",
|
||||||
Forbidden = "forbidden",
|
Forbidden = "forbidden",
|
||||||
}
|
}
|
||||||
|
|
||||||
enum HistoryVisibility {
|
export enum HistoryVisibility {
|
||||||
Invited = "invited",
|
Invited = "invited",
|
||||||
Joined = "joined",
|
Joined = "joined",
|
||||||
Shared = "shared",
|
Shared = "shared",
|
||||||
|
@ -121,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
||||||
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
|
if (refreshWhenTypes.includes(e.getType())) this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEncryptionChange = (e: React.ChangeEvent) => {
|
private onEncryptionChange = () => {
|
||||||
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
|
Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, {
|
||||||
title: _t('Enable encryption?'),
|
title: _t('Enable encryption?'),
|
||||||
description: _t(
|
description: _t(
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {_t} from "../../../../../languageHandler";
|
import {_t} from "../../../../../languageHandler";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import CallMediaHandler from "../../../../../CallMediaHandler";
|
import MediaDeviceHandler from "../../../../../MediaDeviceHandler";
|
||||||
import Field from "../../../elements/Field";
|
import Field from "../../../elements/Field";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||||
|
@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices();
|
const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices();
|
||||||
if (canSeeDeviceLabels) {
|
if (canSeeDeviceLabels) {
|
||||||
this._refreshMediaDevices();
|
this._refreshMediaDevices();
|
||||||
}
|
}
|
||||||
|
@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
|
|
||||||
_refreshMediaDevices = async (stream) => {
|
_refreshMediaDevices = async (stream) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
mediaDevices: await CallMediaHandler.getDevices(),
|
mediaDevices: await MediaDeviceHandler.getDevices(),
|
||||||
activeAudioOutput: CallMediaHandler.getAudioOutput(),
|
activeAudioOutput: MediaDeviceHandler.getAudioOutput(),
|
||||||
activeAudioInput: CallMediaHandler.getAudioInput(),
|
activeAudioInput: MediaDeviceHandler.getAudioInput(),
|
||||||
activeVideoInput: CallMediaHandler.getVideoInput(),
|
activeVideoInput: MediaDeviceHandler.getVideoInput(),
|
||||||
});
|
});
|
||||||
if (stream) {
|
if (stream) {
|
||||||
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
// kill stream (after we've enumerated the devices, otherwise we'd get empty labels again)
|
||||||
|
@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_setAudioOutput = (e) => {
|
_setAudioOutput = (e) => {
|
||||||
CallMediaHandler.setAudioOutput(e.target.value);
|
MediaDeviceHandler.instance.setAudioOutput(e.target.value);
|
||||||
this.setState({
|
this.setState({
|
||||||
activeAudioOutput: e.target.value,
|
activeAudioOutput: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_setAudioInput = (e) => {
|
_setAudioInput = (e) => {
|
||||||
CallMediaHandler.setAudioInput(e.target.value);
|
MediaDeviceHandler.instance.setAudioInput(e.target.value);
|
||||||
this.setState({
|
this.setState({
|
||||||
activeAudioInput: e.target.value,
|
activeAudioInput: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_setVideoInput = (e) => {
|
_setVideoInput = (e) => {
|
||||||
CallMediaHandler.setVideoInput(e.target.value);
|
MediaDeviceHandler.instance.setVideoInput(e.target.value);
|
||||||
this.setState({
|
this.setState({
|
||||||
activeVideoInput: e.target.value,
|
activeVideoInput: e.target.value,
|
||||||
});
|
});
|
||||||
|
@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
|
const audioOutputs = this.state.mediaDevices.audioOutput.slice(0);
|
||||||
if (audioOutputs.length > 0) {
|
if (audioOutputs.length > 0) {
|
||||||
const defaultDevice = getDefaultDevice(audioOutputs);
|
const defaultDevice = getDefaultDevice(audioOutputs);
|
||||||
speakerDropdown = (
|
speakerDropdown = (
|
||||||
|
@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
const audioInputs = this.state.mediaDevices.audioInput.slice(0);
|
||||||
if (audioInputs.length > 0) {
|
if (audioInputs.length > 0) {
|
||||||
const defaultDevice = getDefaultDevice(audioInputs);
|
const defaultDevice = getDefaultDevice(audioInputs);
|
||||||
microphoneDropdown = (
|
microphoneDropdown = (
|
||||||
|
@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoInputs = this.state.mediaDevices.videoinput.slice(0);
|
const videoInputs = this.state.mediaDevices.videoInput.slice(0);
|
||||||
if (videoInputs.length > 0) {
|
if (videoInputs.length > 0) {
|
||||||
const defaultDevice = getDefaultDevice(videoInputs);
|
const defaultDevice = getDefaultDevice(videoInputs);
|
||||||
webcamDropdown = (
|
webcamDropdown = (
|
||||||
|
|
|
@ -35,6 +35,7 @@ import withValidation from "../elements/Validation";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
|
import { ICreateRoomStateEvent } from "matrix-js-sdk/src/@types/requests";
|
||||||
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
|
|
||||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +61,11 @@ const spaceNameValidator = withValidation({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nameToAlias = (name: string, domain: string): string => {
|
||||||
|
const localpart = name.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9_-]+/gi, "");
|
||||||
|
return `#${localpart}:${domain}`;
|
||||||
|
};
|
||||||
|
|
||||||
const SpaceCreateMenu = ({ onFinished }) => {
|
const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||||
|
@ -67,6 +73,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const spaceNameField = useRef<Field>();
|
const spaceNameField = useRef<Field>();
|
||||||
|
const [alias, setAlias] = useState("");
|
||||||
|
const spaceAliasField = useRef<RoomAliasField>();
|
||||||
const [avatar, setAvatar] = useState<File>(null);
|
const [avatar, setAvatar] = useState<File>(null);
|
||||||
const [topic, setTopic] = useState<string>("");
|
const [topic, setTopic] = useState<string>("");
|
||||||
|
|
||||||
|
@ -82,6 +90,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// validate the space name alias field but do not require it
|
||||||
|
if (visibility === Visibility.Public && !await spaceAliasField.current.validate({ allowEmpty: true })) {
|
||||||
|
spaceAliasField.current.focus();
|
||||||
|
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const initialState: ICreateRoomStateEvent[] = [
|
const initialState: ICreateRoomStateEvent[] = [
|
||||||
{
|
{
|
||||||
|
@ -99,12 +114,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
content: { url },
|
content: { url },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (topic) {
|
|
||||||
initialState.push({
|
|
||||||
type: EventType.RoomTopic,
|
|
||||||
content: { topic },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createRoom({
|
await createRoom({
|
||||||
|
@ -112,7 +121,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||||
name,
|
name,
|
||||||
creation_content: {
|
creation_content: {
|
||||||
// Based on MSC1840
|
|
||||||
[RoomCreateTypeField]: RoomType.Space,
|
[RoomCreateTypeField]: RoomType.Space,
|
||||||
},
|
},
|
||||||
initial_state: initialState,
|
initial_state: initialState,
|
||||||
|
@ -121,6 +129,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
events_default: 100,
|
events_default: 100,
|
||||||
...Visibility.Public ? { invite: 0 } : {},
|
...Visibility.Public ? { invite: 0 } : {},
|
||||||
},
|
},
|
||||||
|
room_alias_name: alias ? alias.substr(1, alias.indexOf(":") - 1) : undefined,
|
||||||
|
topic,
|
||||||
},
|
},
|
||||||
spinner: false,
|
spinner: false,
|
||||||
encryption: false,
|
encryption: false,
|
||||||
|
@ -159,6 +169,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
<SpaceFeedbackPrompt onClick={onFinished} />
|
<SpaceFeedbackPrompt onClick={onFinished} />
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
} else {
|
} else {
|
||||||
|
const domain = cli.getDomain();
|
||||||
body = <React.Fragment>
|
body = <React.Fragment>
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className="mx_SpaceCreateMenu_back"
|
className="mx_SpaceCreateMenu_back"
|
||||||
|
@ -187,12 +198,30 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
label={_t("Name")}
|
label={_t("Name")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={ev => setName(ev.target.value)}
|
onChange={ev => {
|
||||||
|
const newName = ev.target.value;
|
||||||
|
if (!alias || alias === nameToAlias(name, domain)) {
|
||||||
|
setAlias(nameToAlias(newName, domain));
|
||||||
|
}
|
||||||
|
setName(newName);
|
||||||
|
}}
|
||||||
ref={spaceNameField}
|
ref={spaceNameField}
|
||||||
onValidate={spaceNameValidator}
|
onValidate={spaceNameValidator}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{ visibility === Visibility.Public
|
||||||
|
? <RoomAliasField
|
||||||
|
ref={spaceAliasField}
|
||||||
|
onChange={setAlias}
|
||||||
|
domain={domain}
|
||||||
|
value={alias}
|
||||||
|
placeholder={name ? nameToAlias(name, domain) : _t("e.g. my-space")}
|
||||||
|
label={_t("Address")}
|
||||||
|
/>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name="spaceTopic"
|
name="spaceTopic"
|
||||||
element="textarea"
|
element="textarea"
|
||||||
|
|
|
@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import {useContextMenu} from "../../structures/ContextMenu";
|
import { useContextMenu } from "../../structures/ContextMenu";
|
||||||
import SpaceCreateMenu from "./SpaceCreateMenu";
|
import SpaceCreateMenu from "./SpaceCreateMenu";
|
||||||
import {SpaceItem} from "./SpaceTreeLevel";
|
import { SpaceItem } from "./SpaceTreeLevel";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import SpaceStore, {
|
import SpaceStore, {
|
||||||
HOME_SPACE,
|
HOME_SPACE,
|
||||||
UPDATE_INVITED_SPACES,
|
UPDATE_INVITED_SPACES,
|
||||||
|
@ -38,9 +39,9 @@ import {
|
||||||
RovingAccessibleTooltipButton,
|
RovingAccessibleTooltipButton,
|
||||||
RovingTabIndexProvider,
|
RovingTabIndexProvider,
|
||||||
} from "../../../accessibility/RovingTabIndex";
|
} from "../../../accessibility/RovingTabIndex";
|
||||||
import {Key} from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
import {NotificationState} from "../../../stores/notifications/NotificationState";
|
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IButtonProps {
|
interface IButtonProps {
|
||||||
|
@ -122,11 +123,65 @@ const useSpaces = (): [Room[], Room[], Room | null] => {
|
||||||
return [invites, spaces, activeSpace];
|
return [invites, spaces, activeSpace];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IInnerSpacePanelProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
isPanelCollapsed: boolean;
|
||||||
|
setPanelCollapsed: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation
|
||||||
|
const InnerSpacePanel = React.memo<IInnerSpacePanelProps>(({ children, isPanelCollapsed, setPanelCollapsed }) => {
|
||||||
|
const [invites, spaces, activeSpace] = useSpaces();
|
||||||
|
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||||
|
|
||||||
|
const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
|
||||||
|
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
|
||||||
|
|
||||||
|
return <div className="mx_SpaceTreeLevel">
|
||||||
|
<SpaceButton
|
||||||
|
className="mx_SpaceButton_home"
|
||||||
|
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||||
|
selected={!activeSpace}
|
||||||
|
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
|
||||||
|
notificationState={homeNotificationState}
|
||||||
|
isNarrow={isPanelCollapsed}
|
||||||
|
/>
|
||||||
|
{ invites.map(s => (
|
||||||
|
<SpaceItem
|
||||||
|
key={s.roomId}
|
||||||
|
space={s}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
isPanelCollapsed={isPanelCollapsed}
|
||||||
|
onExpand={() => setPanelCollapsed(false)}
|
||||||
|
/>
|
||||||
|
)) }
|
||||||
|
{ spaces.map((s, i) => (
|
||||||
|
<Draggable key={s.roomId} draggableId={s.roomId} index={i}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<SpaceItem
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
key={s.roomId}
|
||||||
|
innerRef={provided.innerRef}
|
||||||
|
className={snapshot.isDragging
|
||||||
|
? "mx_SpaceItem_dragging"
|
||||||
|
: undefined}
|
||||||
|
space={s}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
isPanelCollapsed={isPanelCollapsed}
|
||||||
|
onExpand={() => setPanelCollapsed(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
)) }
|
||||||
|
{ children }
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
const SpacePanel = () => {
|
const SpacePanel = () => {
|
||||||
// We don't need the handle as we position the menu in a constant location
|
// We don't need the handle as we position the menu in a constant location
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||||
const [invites, spaces, activeSpace] = useSpaces();
|
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -135,10 +190,6 @@ const SpacePanel = () => {
|
||||||
}
|
}
|
||||||
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const newClasses = classNames("mx_SpaceButton_new", {
|
|
||||||
mx_SpaceButton_newCancel: menuDisplayed,
|
|
||||||
});
|
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
|
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
|
||||||
|
@ -205,63 +256,61 @@ const SpacePanel = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
const onNewClick = menuDisplayed ? closeMenu : () => {
|
||||||
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
|
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||||
|
openMenu();
|
||||||
|
};
|
||||||
|
|
||||||
const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms")
|
return (
|
||||||
? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE);
|
<DragDropContext onDragEnd={result => {
|
||||||
|
if (!result.destination) return; // dropped outside the list
|
||||||
|
SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index);
|
||||||
|
}}>
|
||||||
|
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||||
|
{({onKeyDownHandler}) => (
|
||||||
|
<ul
|
||||||
|
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
|
>
|
||||||
|
<Droppable droppableId="top-level-spaces">
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<AutoHideScrollbar
|
||||||
|
{...provided.droppableProps}
|
||||||
|
wrappedRef={provided.innerRef}
|
||||||
|
className="mx_SpacePanel_spaceTreeWrapper"
|
||||||
|
style={snapshot.isDraggingOver ? {
|
||||||
|
pointerEvents: "none",
|
||||||
|
} : undefined}
|
||||||
|
>
|
||||||
|
<InnerSpacePanel
|
||||||
|
isPanelCollapsed={isPanelCollapsed}
|
||||||
|
setPanelCollapsed={setPanelCollapsed}
|
||||||
|
>
|
||||||
|
{ provided.placeholder }
|
||||||
|
</InnerSpacePanel>
|
||||||
|
|
||||||
// TODO drag and drop for re-arranging order
|
<SpaceButton
|
||||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
className={classNames("mx_SpaceButton_new", {
|
||||||
{({onKeyDownHandler}) => (
|
mx_SpaceButton_newCancel: menuDisplayed,
|
||||||
<ul
|
})}
|
||||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||||
onKeyDown={onKeyDownHandler}
|
onClick={onNewClick}
|
||||||
>
|
isNarrow={isPanelCollapsed}
|
||||||
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
|
/>
|
||||||
<div className="mx_SpaceTreeLevel">
|
</AutoHideScrollbar>
|
||||||
<SpaceButton
|
)}
|
||||||
className="mx_SpaceButton_home"
|
</Droppable>
|
||||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
<AccessibleTooltipButton
|
||||||
selected={!activeSpace}
|
className={classNames("mx_SpacePanel_toggleCollapse", { expanded: !isPanelCollapsed })}
|
||||||
tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")}
|
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||||
notificationState={homeNotificationState}
|
title={isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel")}
|
||||||
isNarrow={isPanelCollapsed}
|
|
||||||
/>
|
/>
|
||||||
{ invites.map(s => <SpaceItem
|
{ contextMenu }
|
||||||
key={s.roomId}
|
</ul>
|
||||||
space={s}
|
)}
|
||||||
activeSpaces={activeSpaces}
|
</RovingTabIndexProvider>
|
||||||
isPanelCollapsed={isPanelCollapsed}
|
</DragDropContext>
|
||||||
onExpand={() => setPanelCollapsed(false)}
|
);
|
||||||
/>) }
|
|
||||||
{ spaces.map(s => <SpaceItem
|
|
||||||
key={s.roomId}
|
|
||||||
space={s}
|
|
||||||
activeSpaces={activeSpaces}
|
|
||||||
isPanelCollapsed={isPanelCollapsed}
|
|
||||||
onExpand={() => setPanelCollapsed(false)}
|
|
||||||
/>) }
|
|
||||||
</div>
|
|
||||||
<SpaceButton
|
|
||||||
className={newClasses}
|
|
||||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
|
||||||
onClick={menuDisplayed ? closeMenu : () => {
|
|
||||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
|
||||||
openMenu();
|
|
||||||
}}
|
|
||||||
isNarrow={isPanelCollapsed}
|
|
||||||
/>
|
|
||||||
</AutoHideScrollbar>
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
|
||||||
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
|
||||||
title={expandCollapseButtonTitle}
|
|
||||||
/>
|
|
||||||
{ contextMenu }
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</RovingTabIndexProvider>
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SpacePanel;
|
export default SpacePanel;
|
||||||
|
|
143
src/components/views/spaces/SpaceSettingsGeneralTab.tsx
Normal file
143
src/components/views/spaces/SpaceSettingsGeneralTab.tsx
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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, { useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
|
||||||
|
import SpaceBasicSettings from "./SpaceBasicSettings";
|
||||||
|
import { avatarUrlForRoom } from "../../../Avatar";
|
||||||
|
import { IDialogProps } from "../dialogs/IDialogProps";
|
||||||
|
import { getTopic } from "../elements/RoomTopic";
|
||||||
|
import { defaultDispatcher } from "../../../dispatcher/dispatcher";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
space: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProps) => {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
|
||||||
|
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||||
|
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||||
|
const avatarChanged = newAvatar !== null;
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>(space.name);
|
||||||
|
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||||
|
const nameChanged = name !== space.name;
|
||||||
|
|
||||||
|
const currentTopic = getTopic(space);
|
||||||
|
const [topic, setTopic] = useState<string>(currentTopic);
|
||||||
|
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||||
|
const topicChanged = topic !== currentTopic;
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setNewAvatar(null);
|
||||||
|
setName(space.name);
|
||||||
|
setTopic(currentTopic);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSave = async () => {
|
||||||
|
setBusy(true);
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
if (avatarChanged) {
|
||||||
|
if (newAvatar) {
|
||||||
|
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||||
|
url: await cli.uploadContent(newAvatar),
|
||||||
|
}, ""));
|
||||||
|
} else {
|
||||||
|
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameChanged) {
|
||||||
|
promises.push(cli.setRoomName(space.roomId, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topicChanged) {
|
||||||
|
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
setBusy(false);
|
||||||
|
const failures = results.filter(r => r.status === "rejected");
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error("Failed to save space settings: ", failures);
|
||||||
|
setError(_t("Failed to save space settings."));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className="mx_SettingsTab">
|
||||||
|
<div className="mx_SettingsTab_heading">{ _t("General") }</div>
|
||||||
|
|
||||||
|
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||||
|
|
||||||
|
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||||
|
|
||||||
|
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
|
||||||
|
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
<SpaceBasicSettings
|
||||||
|
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||||
|
avatarDisabled={busy || !canSetAvatar}
|
||||||
|
setAvatar={setNewAvatar}
|
||||||
|
name={name}
|
||||||
|
nameDisabled={busy || !canSetName}
|
||||||
|
setName={setName}
|
||||||
|
topic={topic}
|
||||||
|
topicDisabled={busy || !canSetTopic}
|
||||||
|
setTopic={setTopic}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={busy || !(avatarChanged || nameChanged || topicChanged)}
|
||||||
|
kind="link"
|
||||||
|
>
|
||||||
|
{ _t("Cancel") }
|
||||||
|
</AccessibleButton>
|
||||||
|
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
|
||||||
|
{ busy ? _t("Saving...") : _t("Save Changes") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="mx_SettingsTab_subheading">{_t("Leave Space")}</span>
|
||||||
|
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||||
|
<AccessibleButton
|
||||||
|
kind="danger"
|
||||||
|
onClick={() => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: "leave_room",
|
||||||
|
room_id: space.roomId,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ _t("Leave Space") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpaceSettingsGeneralTab;
|
187
src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
Normal file
187
src/components/views/spaces/SpaceSettingsVisibilityTab.tsx
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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, { useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import AliasSettings from "../room_settings/AliasSettings";
|
||||||
|
import { useStateToggle } from "../../../hooks/useStateToggle";
|
||||||
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
|
import { GuestAccess, HistoryVisibility, JoinRule } from "../settings/tabs/room/SecurityRoomSettingsTab";
|
||||||
|
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
matrixClient: MatrixClient;
|
||||||
|
space: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SpaceVisibility {
|
||||||
|
Unlisted = "unlisted",
|
||||||
|
Private = "private",
|
||||||
|
}
|
||||||
|
|
||||||
|
const useLocalEcho = <T extends any>(
|
||||||
|
currentFactory: () => T,
|
||||||
|
setterFn: (value: T) => Promise<void>,
|
||||||
|
errorFn: (error: Error) => void,
|
||||||
|
): [value: T, handler: (value: T) => void] => {
|
||||||
|
const [value, setValue] = useState(currentFactory);
|
||||||
|
const handler = async (value: T) => {
|
||||||
|
setValue(value);
|
||||||
|
try {
|
||||||
|
await setterFn(value);
|
||||||
|
} catch (e) {
|
||||||
|
setValue(currentFactory());
|
||||||
|
errorFn(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [value, handler];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => {
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const userId = cli.getUserId();
|
||||||
|
|
||||||
|
const [visibility, setVisibility] = useLocalEcho<SpaceVisibility>(
|
||||||
|
() => space.getJoinRule() === JoinRule.Private ? SpaceVisibility.Private : SpaceVisibility.Unlisted,
|
||||||
|
visibility => cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, {
|
||||||
|
join_rule: visibility === SpaceVisibility.Unlisted ? JoinRule.Public : JoinRule.Private,
|
||||||
|
}, ""),
|
||||||
|
() => setError(_t("Failed to update the visibility of this space")),
|
||||||
|
);
|
||||||
|
const [guestAccessEnabled, setGuestAccessEnabled] = useLocalEcho<boolean>(
|
||||||
|
() => space.currentState.getStateEvents(EventType.RoomGuestAccess, "")
|
||||||
|
?.getContent()?.guest_access === GuestAccess.CanJoin,
|
||||||
|
guestAccessEnabled => cli.sendStateEvent(space.roomId, EventType.RoomGuestAccess, {
|
||||||
|
guest_access: guestAccessEnabled ? GuestAccess.CanJoin : GuestAccess.Forbidden,
|
||||||
|
}, ""),
|
||||||
|
() => setError(_t("Failed to update the guest access of this space")),
|
||||||
|
);
|
||||||
|
const [historyVisibility, setHistoryVisibility] = useLocalEcho<HistoryVisibility>(
|
||||||
|
() => space.currentState.getStateEvents(EventType.RoomHistoryVisibility, "")
|
||||||
|
?.getContent()?.history_visibility || HistoryVisibility.Shared,
|
||||||
|
historyVisibility => cli.sendStateEvent(space.roomId, EventType.RoomHistoryVisibility, {
|
||||||
|
history_visibility: historyVisibility,
|
||||||
|
}, ""),
|
||||||
|
() => setError(_t("Failed to update the history visibility of this space")),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showAdvancedSection, toggleAdvancedSection] = useStateToggle();
|
||||||
|
|
||||||
|
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||||
|
const canSetGuestAccess = space.currentState.maySendStateEvent(EventType.RoomGuestAccess, userId);
|
||||||
|
const canSetHistoryVisibility = space.currentState.maySendStateEvent(EventType.RoomHistoryVisibility, userId);
|
||||||
|
const canSetCanonical = space.currentState.mayClientSendStateEvent(EventType.RoomCanonicalAlias, cli);
|
||||||
|
const canonicalAliasEv = space.currentState.getStateEvents(EventType.RoomCanonicalAlias, "");
|
||||||
|
|
||||||
|
let advancedSection;
|
||||||
|
if (showAdvancedSection) {
|
||||||
|
advancedSection = <>
|
||||||
|
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||||
|
{ _t("Hide advanced") }
|
||||||
|
</AccessibleButton>
|
||||||
|
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={guestAccessEnabled}
|
||||||
|
onChange={setGuestAccessEnabled}
|
||||||
|
disabled={!canSetGuestAccess}
|
||||||
|
label={_t("Enable guest access")}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
{ _t("Guests can join a space without having an account.") }
|
||||||
|
<br />
|
||||||
|
{ _t("This may be useful for public spaces.") }
|
||||||
|
</p>
|
||||||
|
</>;
|
||||||
|
} else {
|
||||||
|
advancedSection = <>
|
||||||
|
<AccessibleButton onClick={toggleAdvancedSection} kind="link" className="mx_SettingsTab_showAdvanced">
|
||||||
|
{ _t("Show advanced") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let addressesSection;
|
||||||
|
if (visibility !== SpaceVisibility.Private) {
|
||||||
|
addressesSection = <>
|
||||||
|
<span className="mx_SettingsTab_subheading">{_t("Address")}</span>
|
||||||
|
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
|
||||||
|
<AliasSettings
|
||||||
|
roomId={space.roomId}
|
||||||
|
canSetCanonicalAlias={canSetCanonical}
|
||||||
|
canSetAliases={true}
|
||||||
|
canonicalAliasEvent={canonicalAliasEv}
|
||||||
|
hidePublishSetting={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_SettingsTab">
|
||||||
|
<div className="mx_SettingsTab_heading">{ _t("Visibility") }</div>
|
||||||
|
|
||||||
|
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||||
|
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
<div className="mx_SettingsTab_section_caption">
|
||||||
|
{ _t("Decide who can view and join %(spaceName)s.", { spaceName: space.name }) }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<StyledRadioGroup
|
||||||
|
name="spaceVisibility"
|
||||||
|
value={visibility}
|
||||||
|
onChange={setVisibility}
|
||||||
|
disabled={!canSetJoinRule}
|
||||||
|
definitions={[
|
||||||
|
{
|
||||||
|
value: SpaceVisibility.Unlisted,
|
||||||
|
label: _t("Public"),
|
||||||
|
description: _t("anyone with the link can view and join"),
|
||||||
|
}, {
|
||||||
|
value: SpaceVisibility.Private,
|
||||||
|
label: _t("Invite only"),
|
||||||
|
description: _t("only invited people can view and join"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ advancedSection }
|
||||||
|
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
value={historyVisibility === HistoryVisibility.WorldReadable}
|
||||||
|
onChange={(checked: boolean) => {
|
||||||
|
setHistoryVisibility(checked ? HistoryVisibility.WorldReadable : HistoryVisibility.Shared);
|
||||||
|
}}
|
||||||
|
disabled={!canSetHistoryVisibility}
|
||||||
|
label={_t("Preview Space")}
|
||||||
|
/>
|
||||||
|
<div>{ _t("Allow people to preview your space before they join.") }</div>
|
||||||
|
<b>{ _t("Recommended for public spaces.") }</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ addressesSection }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpaceSettingsVisibilityTab;
|
|
@ -14,23 +14,22 @@ 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, InputHTMLAttributes, LegacyRef } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import SpaceStore from "../../../stores/SpaceStore";
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
|
import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore";
|
||||||
import NotificationBadge from "../rooms/NotificationBadge";
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
|
import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||||
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||||
import {toRightOf} from "../../structures/ContextMenu";
|
import { toRightOf } from "../../structures/ContextMenu";
|
||||||
import {
|
import {
|
||||||
shouldShowSpaceSettings,
|
shouldShowSpaceSettings,
|
||||||
showAddExistingRooms,
|
showAddExistingRooms,
|
||||||
|
@ -39,33 +38,38 @@ import {
|
||||||
showSpaceSettings,
|
showSpaceSettings,
|
||||||
} from "../../../utils/space";
|
} from "../../../utils/space";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
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 { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
||||||
|
|
||||||
interface IItemProps {
|
interface IItemProps extends InputHTMLAttributes<HTMLLIElement> {
|
||||||
space?: Room;
|
space?: Room;
|
||||||
activeSpaces: Room[];
|
activeSpaces: Room[];
|
||||||
isNested?: boolean;
|
isNested?: boolean;
|
||||||
isPanelCollapsed?: boolean;
|
isPanelCollapsed?: boolean;
|
||||||
onExpand?: Function;
|
onExpand?: Function;
|
||||||
parents?: Set<string>;
|
parents?: Set<string>;
|
||||||
|
innerRef?: LegacyRef<HTMLLIElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IItemState {
|
interface IItemState {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
|
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
|
||||||
|
childSpaces: Room[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
private buttonRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -78,14 +82,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsed: collapsed,
|
collapsed: collapsed,
|
||||||
contextMenuPosition: null,
|
contextMenuPosition: null,
|
||||||
|
childSpaces: this.childSpaces,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleCollapse(evt) {
|
componentWillUnmount() {
|
||||||
if (this.props.onExpand && this.state.collapsed) {
|
SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSpaceUpdate = () => {
|
||||||
|
this.setState({
|
||||||
|
childSpaces: this.childSpaces,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private get childSpaces() {
|
||||||
|
return SpaceStore.instance.getChildSpaces(this.props.space.roomId)
|
||||||
|
.filter(s => !this.props.parents?.has(s.roomId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get isCollapsed() {
|
||||||
|
return this.state.collapsed || this.props.isPanelCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleCollapse = evt => {
|
||||||
|
if (this.props.onExpand && this.isCollapsed) {
|
||||||
this.props.onExpand();
|
this.props.onExpand();
|
||||||
}
|
}
|
||||||
const newCollapsedState = !this.state.collapsed;
|
const newCollapsedState = !this.isCollapsed;
|
||||||
|
|
||||||
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
|
SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState(
|
||||||
this.props.space.roomId,
|
this.props.space.roomId,
|
||||||
|
@ -96,7 +122,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
// don't bubble up so encapsulating button for space
|
// don't bubble up so encapsulating button for space
|
||||||
// doesn't get triggered
|
// doesn't get triggered
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
}
|
};
|
||||||
|
|
||||||
private onContextMenu = (ev: React.MouseEvent) => {
|
private onContextMenu = (ev: React.MouseEvent) => {
|
||||||
if (this.props.space.getMyMembership() !== "join") return;
|
if (this.props.space.getMyMembership() !== "join") return;
|
||||||
|
@ -111,6 +137,43 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
let handled = true;
|
||||||
|
const action = getKeyBindingsManager().getRoomListAction(ev);
|
||||||
|
const hasChildren = this.state.childSpaces?.length;
|
||||||
|
switch (action) {
|
||||||
|
case RoomListAction.CollapseSection:
|
||||||
|
if (hasChildren && !this.isCollapsed) {
|
||||||
|
this.toggleCollapse(ev);
|
||||||
|
} else {
|
||||||
|
const parentItem = this.buttonRef?.current?.parentElement?.parentElement;
|
||||||
|
const parentButton = parentItem?.previousElementSibling as HTMLElement;
|
||||||
|
parentButton?.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RoomListAction.ExpandSection:
|
||||||
|
if (hasChildren) {
|
||||||
|
if (this.isCollapsed) {
|
||||||
|
this.toggleCollapse(ev);
|
||||||
|
} else {
|
||||||
|
const childLevel = this.buttonRef?.current?.nextElementSibling;
|
||||||
|
const firstSpaceItemChild = childLevel?.querySelector<HTMLLIElement>(".mx_SpaceItem");
|
||||||
|
firstSpaceItemChild?.querySelector<HTMLDivElement>(".mx_SpaceButton")?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
handled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onClick = (ev: React.MouseEvent) => {
|
private onClick = (ev: React.MouseEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -300,27 +363,25 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {space, activeSpaces, isNested} = this.props;
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { space, activeSpaces, isNested, isPanelCollapsed, onExpand, parents, innerRef,
|
||||||
|
...otherProps } = this.props;
|
||||||
|
|
||||||
const forceCollapsed = this.props.isPanelCollapsed;
|
const collapsed = this.isCollapsed;
|
||||||
const isNarrow = this.props.isPanelCollapsed;
|
|
||||||
const collapsed = this.state.collapsed || forceCollapsed;
|
|
||||||
|
|
||||||
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId)
|
|
||||||
.filter(s => !this.props.parents?.has(s.roomId));
|
|
||||||
const isActive = activeSpaces.includes(space);
|
const isActive = activeSpaces.includes(space);
|
||||||
const itemClasses = classNames({
|
const itemClasses = classNames(this.props.className, {
|
||||||
"mx_SpaceItem": true,
|
"mx_SpaceItem": true,
|
||||||
"mx_SpaceItem_narrow": isNarrow,
|
"mx_SpaceItem_narrow": isPanelCollapsed,
|
||||||
"collapsed": collapsed,
|
"collapsed": collapsed,
|
||||||
"hasSubSpaces": childSpaces && childSpaces.length,
|
"hasSubSpaces": this.state.childSpaces?.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isInvite = space.getMyMembership() === "invite";
|
const isInvite = space.getMyMembership() === "invite";
|
||||||
const classes = classNames("mx_SpaceButton", {
|
const classes = classNames("mx_SpaceButton", {
|
||||||
mx_SpaceButton_active: isActive,
|
mx_SpaceButton_active: isActive,
|
||||||
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||||
mx_SpaceButton_narrow: isNarrow,
|
mx_SpaceButton_narrow: isPanelCollapsed,
|
||||||
mx_SpaceButton_invite: isInvite,
|
mx_SpaceButton_invite: isInvite,
|
||||||
});
|
});
|
||||||
const notificationState = isInvite
|
const notificationState = isInvite
|
||||||
|
@ -328,12 +389,12 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
: SpaceStore.instance.getNotificationState(space.roomId);
|
: SpaceStore.instance.getNotificationState(space.roomId);
|
||||||
|
|
||||||
let childItems;
|
let childItems;
|
||||||
if (childSpaces && !collapsed) {
|
if (this.state.childSpaces?.length && !collapsed) {
|
||||||
childItems = <SpaceTreeLevel
|
childItems = <SpaceTreeLevel
|
||||||
spaces={childSpaces}
|
spaces={this.state.childSpaces}
|
||||||
activeSpaces={activeSpaces}
|
activeSpaces={activeSpaces}
|
||||||
isNested={true}
|
isNested={true}
|
||||||
parents={new Set(this.props.parents).add(this.props.space.roomId)}
|
parents={new Set(parents).add(space.roomId)}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,53 +407,36 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
|
|
||||||
const avatarSize = isNested ? 24 : 32;
|
const avatarSize = isNested ? 24 : 32;
|
||||||
|
|
||||||
const toggleCollapseButton = childSpaces && childSpaces.length ?
|
const toggleCollapseButton = this.state.childSpaces?.length ?
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_SpaceButton_toggleCollapse"
|
className="mx_SpaceButton_toggleCollapse"
|
||||||
onClick={evt => this.toggleCollapse(evt)}
|
onClick={this.toggleCollapse}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={collapsed ? _t("Expand") : _t("Collapse")}
|
||||||
/> : null;
|
/> : null;
|
||||||
|
|
||||||
let button;
|
return (
|
||||||
if (isNarrow) {
|
<li {...otherProps} className={itemClasses} ref={innerRef}>
|
||||||
button = (
|
|
||||||
<RovingAccessibleTooltipButton
|
<RovingAccessibleTooltipButton
|
||||||
className={classes}
|
className={classes}
|
||||||
title={space.name}
|
title={space.name}
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
forceHide={!!this.state.contextMenuPosition}
|
forceHide={!isPanelCollapsed || !!this.state.contextMenuPosition}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
inputRef={this.buttonRef}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
{ toggleCollapseButton }
|
{ toggleCollapseButton }
|
||||||
<div className="mx_SpaceButton_selectionWrapper">
|
<div className="mx_SpaceButton_selectionWrapper">
|
||||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||||
|
{ !isPanelCollapsed && <span className="mx_SpaceButton_name">{ space.name }</span> }
|
||||||
{ notifBadge }
|
{ notifBadge }
|
||||||
{ this.renderContextMenu() }
|
{ this.renderContextMenu() }
|
||||||
</div>
|
</div>
|
||||||
</RovingAccessibleTooltipButton>
|
</RovingAccessibleTooltipButton>
|
||||||
);
|
|
||||||
} else {
|
|
||||||
button = (
|
|
||||||
<RovingAccessibleButton
|
|
||||||
className={classes}
|
|
||||||
onClick={this.onClick}
|
|
||||||
onContextMenu={this.onContextMenu}
|
|
||||||
role="treeitem"
|
|
||||||
>
|
|
||||||
{ toggleCollapseButton }
|
|
||||||
<div className="mx_SpaceButton_selectionWrapper">
|
|
||||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
|
||||||
<span className="mx_SpaceButton_name">{ space.name }</span>
|
|
||||||
{ notifBadge }
|
|
||||||
{ this.renderContextMenu() }
|
|
||||||
</div>
|
|
||||||
</RovingAccessibleButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li className={itemClasses}>
|
|
||||||
{ button }
|
|
||||||
{ childItems }
|
{ childItems }
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||||
import { logger } from 'matrix-js-sdk/src/logger';
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import CallMediaHandler from "../../../CallMediaHandler";
|
import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
feed: CallFeed,
|
feed: CallFeed,
|
||||||
|
@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component<IProps> {
|
||||||
private element = createRef<HTMLAudioElement>();
|
private element = createRef<HTMLAudioElement>();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
MediaDeviceHandler.instance.addListener(
|
||||||
|
MediaDeviceHandlerEvent.AudioOutputChanged,
|
||||||
|
this.onAudioOutputChanged,
|
||||||
|
);
|
||||||
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||||
this.playMedia();
|
this.playMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
MediaDeviceHandler.instance.removeListener(
|
||||||
|
MediaDeviceHandlerEvent.AudioOutputChanged,
|
||||||
|
this.onAudioOutputChanged,
|
||||||
|
);
|
||||||
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||||
this.stopMedia();
|
this.stopMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
private playMedia() {
|
private onAudioOutputChanged = (audioOutput: string) => {
|
||||||
const element = this.element.current;
|
const element = this.element.current;
|
||||||
const audioOutput = CallMediaHandler.getAudioOutput();
|
|
||||||
|
|
||||||
if (audioOutput) {
|
if (audioOutput) {
|
||||||
try {
|
try {
|
||||||
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||||
|
@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component<IProps> {
|
||||||
logger.warn("Couldn't set requested audio output device: using default", e);
|
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private playMedia() {
|
||||||
|
const element = this.element.current;
|
||||||
|
this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput());
|
||||||
element.muted = false;
|
element.muted = false;
|
||||||
element.srcObject = this.props.feed.stream;
|
element.srcObject = this.props.feed.stream;
|
||||||
element.autoplay = true;
|
element.autoplay = true;
|
||||||
|
|
46
src/hooks/useRoomState.ts
Normal file
46
src/hooks/useRoomState.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
|
||||||
|
import { useEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
|
type Mapper<T> = (roomState: RoomState) => T;
|
||||||
|
const defaultMapper: Mapper<RoomState> = (roomState: RoomState) => roomState;
|
||||||
|
|
||||||
|
// Hook to simplify watching Matrix Room state
|
||||||
|
export const useRoomState = <T extends any = RoomState>(
|
||||||
|
room: Room,
|
||||||
|
mapper: Mapper<T> = defaultMapper as Mapper<T>,
|
||||||
|
): T => {
|
||||||
|
const [value, setValue] = useState<T>(room ? mapper(room.currentState) : undefined);
|
||||||
|
|
||||||
|
const update = useCallback(() => {
|
||||||
|
if (!room) return;
|
||||||
|
setValue(mapper(room.currentState));
|
||||||
|
}, [room, mapper]);
|
||||||
|
|
||||||
|
useEventEmitter(room?.currentState, "RoomState.events", update);
|
||||||
|
useEffect(() => {
|
||||||
|
update();
|
||||||
|
return () => {
|
||||||
|
setValue(undefined);
|
||||||
|
};
|
||||||
|
}, [update]);
|
||||||
|
return value;
|
||||||
|
};
|
|
@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react";
|
||||||
|
|
||||||
// Hook to simplify toggling of a boolean state value
|
// Hook to simplify toggling of a boolean state value
|
||||||
// Returns value, method to toggle boolean value and method to set the boolean value
|
// Returns value, method to toggle boolean value and method to set the boolean value
|
||||||
export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch<SetStateAction<boolean>>] => {
|
export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch<SetStateAction<boolean>>] => {
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
const toggleValue = () => {
|
const toggleValue = () => {
|
||||||
setValue(!value);
|
setValue(!value);
|
||||||
|
|
|
@ -396,7 +396,8 @@
|
||||||
"Failed to invite": "Failed to invite",
|
"Failed to invite": "Failed to invite",
|
||||||
"Operation failed": "Operation failed",
|
"Operation failed": "Operation failed",
|
||||||
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
"Failed to invite users to the room:": "Failed to invite users to the room:",
|
||||||
"Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
|
"We sent the others, but the below people couldn't be invited to <RoomName/>": "We sent the others, but the below people couldn't be invited to <RoomName/>",
|
||||||
|
"Some invites couldn't be sent": "Some invites couldn't be sent",
|
||||||
"You need to be logged in.": "You need to be logged in.",
|
"You need to be logged in.": "You need to be logged in.",
|
||||||
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
|
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
|
||||||
"Unable to create widget.": "Unable to create widget.",
|
"Unable to create widget.": "Unable to create widget.",
|
||||||
|
@ -489,24 +490,27 @@
|
||||||
"Converts the room to a DM": "Converts the room to a DM",
|
"Converts the room to a DM": "Converts the room to a DM",
|
||||||
"Converts the DM to a room": "Converts the DM to a room",
|
"Converts the DM to a room": "Converts the DM to a room",
|
||||||
"Displays action": "Displays action",
|
"Displays action": "Displays action",
|
||||||
"Reason": "Reason",
|
"%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s",
|
||||||
"%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
|
"%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation",
|
||||||
"%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
|
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
|
||||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
|
"%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s",
|
||||||
"%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
|
"%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s",
|
||||||
"%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
|
"%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s",
|
||||||
"%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
|
"%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s",
|
||||||
"%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).",
|
"%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)",
|
||||||
"%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.",
|
"%(senderName)s removed their profile picture": "%(senderName)s removed their profile picture",
|
||||||
"%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
|
"%(senderName)s changed their profile picture": "%(senderName)s changed their profile picture",
|
||||||
"%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
|
"%(senderName)s set a profile picture": "%(senderName)s set a profile picture",
|
||||||
"%(senderName)s made no change.": "%(senderName)s made no change.",
|
"%(senderName)s made no change": "%(senderName)s made no change",
|
||||||
"%(targetName)s joined the room.": "%(targetName)s joined the room.",
|
"%(targetName)s joined the room": "%(targetName)s joined the room",
|
||||||
"%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
|
"%(targetName)s rejected the invitation": "%(targetName)s rejected the invitation",
|
||||||
"%(targetName)s left the room.": "%(targetName)s left the room.",
|
"%(targetName)s left the room: %(reason)s": "%(targetName)s left the room: %(reason)s",
|
||||||
"%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
|
"%(targetName)s left the room": "%(targetName)s left the room",
|
||||||
"%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.",
|
"%(senderName)s unbanned %(targetName)s": "%(senderName)s unbanned %(targetName)s",
|
||||||
"%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.",
|
"%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s",
|
||||||
|
"%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s withdrew %(targetName)s's invitation",
|
||||||
|
"%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s kicked %(targetName)s: %(reason)s",
|
||||||
|
"%(senderName)s kicked %(targetName)s": "%(senderName)s kicked %(targetName)s",
|
||||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
||||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
||||||
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
|
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
|
||||||
|
@ -1023,18 +1027,42 @@
|
||||||
"Your private space": "Your private space",
|
"Your private space": "Your private space",
|
||||||
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
|
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
|
||||||
"You can change these anytime.": "You can change these anytime.",
|
"You can change these anytime.": "You can change these anytime.",
|
||||||
|
"e.g. my-space": "e.g. my-space",
|
||||||
|
"Address": "Address",
|
||||||
"Creating...": "Creating...",
|
"Creating...": "Creating...",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Expand space panel": "Expand space panel",
|
|
||||||
"Collapse space panel": "Collapse space panel",
|
|
||||||
"All rooms": "All rooms",
|
"All rooms": "All rooms",
|
||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
|
"Expand space panel": "Expand space panel",
|
||||||
|
"Collapse space panel": "Collapse space panel",
|
||||||
"Click to copy": "Click to copy",
|
"Click to copy": "Click to copy",
|
||||||
"Copied!": "Copied!",
|
"Copied!": "Copied!",
|
||||||
"Failed to copy": "Failed to copy",
|
"Failed to copy": "Failed to copy",
|
||||||
"Share invite link": "Share invite link",
|
"Share invite link": "Share invite link",
|
||||||
"Invite people": "Invite people",
|
"Invite people": "Invite people",
|
||||||
"Invite with email or username": "Invite with email or username",
|
"Invite with email or username": "Invite with email or username",
|
||||||
|
"Failed to save space settings.": "Failed to save space settings.",
|
||||||
|
"General": "General",
|
||||||
|
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
||||||
|
"Saving...": "Saving...",
|
||||||
|
"Save Changes": "Save Changes",
|
||||||
|
"Leave Space": "Leave Space",
|
||||||
|
"Failed to update the visibility of this space": "Failed to update the visibility of this space",
|
||||||
|
"Failed to update the guest access of this space": "Failed to update the guest access of this space",
|
||||||
|
"Failed to update the history visibility of this space": "Failed to update the history visibility of this space",
|
||||||
|
"Hide advanced": "Hide advanced",
|
||||||
|
"Enable guest access": "Enable guest access",
|
||||||
|
"Guests can join a space without having an account.": "Guests can join a space without having an account.",
|
||||||
|
"This may be useful for public spaces.": "This may be useful for public spaces.",
|
||||||
|
"Show advanced": "Show advanced",
|
||||||
|
"Visibility": "Visibility",
|
||||||
|
"Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.",
|
||||||
|
"anyone with the link can view and join": "anyone with the link can view and join",
|
||||||
|
"Invite only": "Invite only",
|
||||||
|
"only invited people can view and join": "only invited people can view and join",
|
||||||
|
"Preview Space": "Preview Space",
|
||||||
|
"Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
|
||||||
|
"Recommended for public spaces.": "Recommended for public spaces.",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Leave space": "Leave space",
|
"Leave space": "Leave space",
|
||||||
"Create new room": "Create new room",
|
"Create new room": "Create new room",
|
||||||
|
@ -1043,6 +1071,8 @@
|
||||||
"Manage & explore rooms": "Manage & explore rooms",
|
"Manage & explore rooms": "Manage & explore rooms",
|
||||||
"Explore rooms": "Explore rooms",
|
"Explore rooms": "Explore rooms",
|
||||||
"Space options": "Space options",
|
"Space options": "Space options",
|
||||||
|
"Expand": "Expand",
|
||||||
|
"Collapse": "Collapse",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
||||||
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
|
"This bridge is managed by <user />.": "This bridge is managed by <user />.",
|
||||||
|
@ -1229,8 +1259,6 @@
|
||||||
"Custom theme URL": "Custom theme URL",
|
"Custom theme URL": "Custom theme URL",
|
||||||
"Add theme": "Add theme",
|
"Add theme": "Add theme",
|
||||||
"Theme": "Theme",
|
"Theme": "Theme",
|
||||||
"Hide advanced": "Hide advanced",
|
|
||||||
"Show advanced": "Show advanced",
|
|
||||||
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
|
||||||
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
|
||||||
"Customise your appearance": "Customise your appearance",
|
"Customise your appearance": "Customise your appearance",
|
||||||
|
@ -1251,7 +1279,6 @@
|
||||||
"Deactivate Account": "Deactivate Account",
|
"Deactivate Account": "Deactivate Account",
|
||||||
"Deactivate account": "Deactivate account",
|
"Deactivate account": "Deactivate account",
|
||||||
"Discovery": "Discovery",
|
"Discovery": "Discovery",
|
||||||
"General": "General",
|
|
||||||
"Legal": "Legal",
|
"Legal": "Legal",
|
||||||
"Credits": "Credits",
|
"Credits": "Credits",
|
||||||
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
"For help with using %(brand)s, click <a>here</a>.": "For help with using %(brand)s, click <a>here</a>.",
|
||||||
|
@ -1357,6 +1384,7 @@
|
||||||
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
|
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
|
||||||
"this room": "this room",
|
"this room": "this room",
|
||||||
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
|
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
|
||||||
|
"Space information": "Space information",
|
||||||
"Room information": "Room information",
|
"Room information": "Room information",
|
||||||
"Internal room ID:": "Internal room ID:",
|
"Internal room ID:": "Internal room ID:",
|
||||||
"Room version": "Room version",
|
"Room version": "Room version",
|
||||||
|
@ -1386,6 +1414,7 @@
|
||||||
"Failed to unban": "Failed to unban",
|
"Failed to unban": "Failed to unban",
|
||||||
"Unban": "Unban",
|
"Unban": "Unban",
|
||||||
"Banned by %(displayName)s": "Banned by %(displayName)s",
|
"Banned by %(displayName)s": "Banned by %(displayName)s",
|
||||||
|
"Reason": "Reason",
|
||||||
"Error changing power level requirement": "Error changing power level requirement",
|
"Error changing power level requirement": "Error changing power level requirement",
|
||||||
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
|
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
|
||||||
"Error changing power level": "Error changing power level",
|
"Error changing power level": "Error changing power level",
|
||||||
|
@ -1682,14 +1711,18 @@
|
||||||
"Error removing address": "Error removing address",
|
"Error removing address": "Error removing address",
|
||||||
"Main address": "Main address",
|
"Main address": "Main address",
|
||||||
"not specified": "not specified",
|
"not specified": "not specified",
|
||||||
|
"This space has no local addresses": "This space has no local addresses",
|
||||||
"This room has no local addresses": "This room has no local addresses",
|
"This room has no local addresses": "This room has no local addresses",
|
||||||
"Local address": "Local address",
|
"Local address": "Local address",
|
||||||
"Published Addresses": "Published Addresses",
|
"Published Addresses": "Published Addresses",
|
||||||
"Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
|
"Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.",
|
||||||
|
"Published addresses can be used by anyone on any server to join your room.": "Published addresses can be used by anyone on any server to join your room.",
|
||||||
|
"To publish an address, it needs to be set as a local address first.": "To publish an address, it needs to be set as a local address first.",
|
||||||
"Other published addresses:": "Other published addresses:",
|
"Other published addresses:": "Other published addresses:",
|
||||||
"No other published addresses yet, add one below": "No other published addresses yet, add one below",
|
"No other published addresses yet, add one below": "No other published addresses yet, add one below",
|
||||||
"New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)",
|
"New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)",
|
||||||
"Local Addresses": "Local Addresses",
|
"Local Addresses": "Local Addresses",
|
||||||
|
"Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)",
|
||||||
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
|
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
|
||||||
"Show more": "Show more",
|
"Show more": "Show more",
|
||||||
"Error updating flair": "Error updating flair",
|
"Error updating flair": "Error updating flair",
|
||||||
|
@ -2026,7 +2059,7 @@
|
||||||
"Room address": "Room address",
|
"Room address": "Room address",
|
||||||
"e.g. my-room": "e.g. my-room",
|
"e.g. my-room": "e.g. my-room",
|
||||||
"Some characters not allowed": "Some characters not allowed",
|
"Some characters not allowed": "Some characters not allowed",
|
||||||
"Please provide a room address": "Please provide a room address",
|
"Please provide an address": "Please provide an address",
|
||||||
"This address is available to use": "This address is available to use",
|
"This address is available to use": "This address is available to use",
|
||||||
"This address is already in use": "This address is already in use",
|
"This address is already in use": "This address is already in use",
|
||||||
"Server Options": "Server Options",
|
"Server Options": "Server Options",
|
||||||
|
@ -2249,7 +2282,6 @@
|
||||||
"Confirm to continue": "Confirm to continue",
|
"Confirm to continue": "Confirm to continue",
|
||||||
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
|
||||||
"Invite by email": "Invite by email",
|
"Invite by email": "Invite by email",
|
||||||
"Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s",
|
|
||||||
"We couldn't create your DM.": "We couldn't create your DM.",
|
"We couldn't create your DM.": "We couldn't create your DM.",
|
||||||
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
|
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
|
||||||
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
|
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
|
||||||
|
@ -2403,14 +2435,8 @@
|
||||||
"Share Room Message": "Share Room Message",
|
"Share Room Message": "Share Room Message",
|
||||||
"Link to selected message": "Link to selected message",
|
"Link to selected message": "Link to selected message",
|
||||||
"Command Help": "Command Help",
|
"Command Help": "Command Help",
|
||||||
"Failed to save space settings.": "Failed to save space settings.",
|
|
||||||
"Space settings": "Space settings",
|
"Space settings": "Space settings",
|
||||||
"Edit settings relating to your space.": "Edit settings relating to your space.",
|
"Settings - %(spaceName)s": "Settings - %(spaceName)s",
|
||||||
"Make this space private": "Make this space private",
|
|
||||||
"Leave Space": "Leave Space",
|
|
||||||
"View dev tools": "View dev tools",
|
|
||||||
"Saving...": "Saving...",
|
|
||||||
"Save Changes": "Save Changes",
|
|
||||||
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
|
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
|
||||||
"Missing session data": "Missing session data",
|
"Missing session data": "Missing session data",
|
||||||
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
||||||
|
@ -2511,6 +2537,8 @@
|
||||||
"Update status": "Update status",
|
"Update status": "Update status",
|
||||||
"Set status": "Set status",
|
"Set status": "Set status",
|
||||||
"Set a new status...": "Set a new status...",
|
"Set a new status...": "Set a new status...",
|
||||||
|
"Move up": "Move up",
|
||||||
|
"Move down": "Move down",
|
||||||
"View Community": "View Community",
|
"View Community": "View Community",
|
||||||
"Unable to start audio streaming.": "Unable to start audio streaming.",
|
"Unable to start audio streaming.": "Unable to start audio streaming.",
|
||||||
"Failed to start livestream": "Failed to start livestream",
|
"Failed to start livestream": "Failed to start livestream",
|
||||||
|
@ -2657,7 +2685,7 @@
|
||||||
"%(count)s messages deleted.|one": "%(count)s message deleted.",
|
"%(count)s messages deleted.|one": "%(count)s message deleted.",
|
||||||
"Your Communities": "Your Communities",
|
"Your Communities": "Your Communities",
|
||||||
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
|
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
|
||||||
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
|
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
|
||||||
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
|
||||||
"Create a new community": "Create a new community",
|
"Create a new community": "Create a new community",
|
||||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
// The following interfaces take their names and member names from seshat and the spec
|
// The following interfaces take their names and member names from seshat and the spec
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
export interface MatrixEvent {
|
export interface IMatrixEvent {
|
||||||
type: string;
|
type: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
content: {};
|
content: {};
|
||||||
|
@ -27,37 +27,37 @@ export interface MatrixEvent {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MatrixProfile {
|
export interface IMatrixProfile {
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
displayname: string;
|
displayname: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CrawlerCheckpoint {
|
export interface ICrawlerCheckpoint {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
token: string;
|
token: string;
|
||||||
fullCrawl?: boolean;
|
fullCrawl?: boolean;
|
||||||
direction: string;
|
direction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultContext {
|
export interface IResultContext {
|
||||||
events_before: [MatrixEvent];
|
events_before: [IMatrixEvent];
|
||||||
events_after: [MatrixEvent];
|
events_after: [IMatrixEvent];
|
||||||
profile_info: Map<string, MatrixProfile>;
|
profile_info: Map<string, IMatrixProfile>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultsElement {
|
export interface IResultsElement {
|
||||||
rank: number;
|
rank: number;
|
||||||
result: MatrixEvent;
|
result: IMatrixEvent;
|
||||||
context: ResultContext;
|
context: IResultContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface ISearchResult {
|
||||||
count: number;
|
count: number;
|
||||||
results: [ResultsElement];
|
results: [IResultsElement];
|
||||||
highlights: [string];
|
highlights: [string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchArgs {
|
export interface ISearchArgs {
|
||||||
search_term: string;
|
search_term: string;
|
||||||
before_limit: number;
|
before_limit: number;
|
||||||
after_limit: number;
|
after_limit: number;
|
||||||
|
@ -65,19 +65,19 @@ export interface SearchArgs {
|
||||||
room_id?: string;
|
room_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventAndProfile {
|
export interface IEventAndProfile {
|
||||||
event: MatrixEvent;
|
event: IMatrixEvent;
|
||||||
profile: MatrixProfile;
|
profile: IMatrixProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadArgs {
|
export interface ILoadArgs {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
fromEvent?: string;
|
fromEvent?: string;
|
||||||
direction?: string;
|
direction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexStats {
|
export interface IIndexStats {
|
||||||
size: number;
|
size: number;
|
||||||
eventCount: number;
|
eventCount: number;
|
||||||
roomCount: number;
|
roomCount: number;
|
||||||
|
@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager {
|
||||||
* Queue up an event to be added to the index.
|
* Queue up an event to be added to the index.
|
||||||
*
|
*
|
||||||
* @param {MatrixEvent} ev The event that should be added to the index.
|
* @param {MatrixEvent} ev The event that should be added to the index.
|
||||||
* @param {MatrixProfile} profile The profile of the event sender at the
|
* @param {IMatrixProfile} profile The profile of the event sender at the
|
||||||
* time of the event receival.
|
* time of the event receival.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise that will resolve when the was queued up for
|
* @return {Promise} A promise that will resolve when the was queued up for
|
||||||
* addition.
|
* addition.
|
||||||
*/
|
*/
|
||||||
async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<void> {
|
async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Get statistical information of the index.
|
* Get statistical information of the index.
|
||||||
*
|
*
|
||||||
* @return {Promise<IndexStats>} A promise that will resolve to the index
|
* @return {Promise<IIndexStats>} A promise that will resolve to the index
|
||||||
* statistics.
|
* statistics.
|
||||||
*/
|
*/
|
||||||
async getStats(): Promise<IndexStats> {
|
async getStats(): Promise<IIndexStats> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Search the event index using the given term for matching events.
|
* Search the event index using the given term for matching events.
|
||||||
*
|
*
|
||||||
* @param {SearchArgs} searchArgs The search configuration for the search,
|
* @param {ISearchArgs} searchArgs The search configuration for the search,
|
||||||
* sets the search term and determines the search result contents.
|
* sets the search term and determines the search result contents.
|
||||||
*
|
*
|
||||||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
* @return {Promise<[ISearchResult]>} A promise that will resolve to an array
|
||||||
* of search results once the search is done.
|
* of search results once the search is done.
|
||||||
*/
|
*/
|
||||||
async searchEventIndex(searchArgs: SearchArgs): Promise<SearchResult> {
|
async searchEventIndex(searchArgs: ISearchArgs): Promise<ISearchResult> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager {
|
||||||
*
|
*
|
||||||
* This is used to add a batch of events to the index.
|
* This is used to add a batch of events to the index.
|
||||||
*
|
*
|
||||||
* @param {[EventAndProfile]} events The list of events and profiles that
|
* @param {[IEventAndProfile]} events The list of events and profiles that
|
||||||
* should be added to the event index.
|
* should be added to the event index.
|
||||||
* @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
|
* @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that
|
||||||
* should be stored in the index which should be used to continue crawling
|
* should be stored in the index which should be used to continue crawling
|
||||||
* the room.
|
* the room.
|
||||||
* @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
|
* @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
|
||||||
* to fetch the current batch of events. This checkpoint will be removed
|
* to fetch the current batch of events. This checkpoint will be removed
|
||||||
* from the index.
|
* from the index.
|
||||||
*
|
*
|
||||||
|
@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager {
|
||||||
* were already added to the index, false otherwise.
|
* were already added to the index, false otherwise.
|
||||||
*/
|
*/
|
||||||
async addHistoricEvents(
|
async addHistoricEvents(
|
||||||
events: [EventAndProfile],
|
events: IEventAndProfile[],
|
||||||
checkpoint: CrawlerCheckpoint | null,
|
checkpoint: ICrawlerCheckpoint | null,
|
||||||
oldCheckpoint: CrawlerCheckpoint | null,
|
oldCheckpoint: ICrawlerCheckpoint | null,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager {
|
||||||
/**
|
/**
|
||||||
* Add a new crawler checkpoint to the index.
|
* Add a new crawler checkpoint to the index.
|
||||||
*
|
*
|
||||||
* @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added
|
* @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added
|
||||||
* to the index.
|
* to the index.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise that will resolve once the checkpoint has
|
* @return {Promise} A promise that will resolve once the checkpoint has
|
||||||
* been stored.
|
* been stored.
|
||||||
*/
|
*/
|
||||||
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
|
async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new crawler checkpoint to the index.
|
* Add a new crawler checkpoint to the index.
|
||||||
*
|
*
|
||||||
* @param {CrawlerCheckpoint} checkpoint The checkpoint that should be
|
* @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be
|
||||||
* removed from the index.
|
* removed from the index.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise that will resolve once the checkpoint has
|
* @return {Promise} A promise that will resolve once the checkpoint has
|
||||||
* been removed.
|
* been removed.
|
||||||
*/
|
*/
|
||||||
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<void> {
|
async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the stored checkpoints from the index.
|
* Load the stored checkpoints from the index.
|
||||||
*
|
*
|
||||||
* @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an
|
* @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an
|
||||||
* array of crawler checkpoints once they have been loaded from the index.
|
* array of crawler checkpoints once they have been loaded from the index.
|
||||||
*/
|
*/
|
||||||
async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
|
async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager {
|
||||||
* @param {string} args.direction The direction to which we should continue
|
* @param {string} args.direction The direction to which we should continue
|
||||||
* loading events from. This is used only if fromEvent is used as well.
|
* loading events from. This is used only if fromEvent is used as well.
|
||||||
*
|
*
|
||||||
* @return {Promise<[EventAndProfile]>} A promise that will resolve to an
|
* @return {Promise<[IEventAndProfile]>} A promise that will resolve to an
|
||||||
* array of Matrix events that contain mxc URLs accompanied with the
|
* array of Matrix events that contain mxc URLs accompanied with the
|
||||||
* historic profile of the sender.
|
* historic profile of the sender.
|
||||||
*/
|
*/
|
||||||
async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
|
async loadFileEvents(args: ILoadArgs): Promise<IEventAndProfile[]> {
|
||||||
throw new Error("Unimplemented");
|
throw new Error("Unimplemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import { sleep } from "../utils/promise";
|
import { sleep } from "../utils/promise";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { SettingLevel } from "../settings/SettingLevel";
|
import { SettingLevel } from "../settings/SettingLevel";
|
||||||
import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
|
import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager";
|
||||||
|
|
||||||
// The time in ms that the crawler will wait loop iterations if there
|
// The time in ms that the crawler will wait loop iterations if there
|
||||||
// have not been any checkpoints to consume in the last iteration.
|
// have not been any checkpoints to consume in the last iteration.
|
||||||
|
@ -45,9 +45,9 @@ interface ICrawler {
|
||||||
* Event indexing class that wraps the platform specific event indexing.
|
* Event indexing class that wraps the platform specific event indexing.
|
||||||
*/
|
*/
|
||||||
export default class EventIndex extends EventEmitter {
|
export default class EventIndex extends EventEmitter {
|
||||||
private crawlerCheckpoints: CrawlerCheckpoint[] = [];
|
private crawlerCheckpoints: ICrawlerCheckpoint[] = [];
|
||||||
private crawler: ICrawler = null;
|
private crawler: ICrawler = null;
|
||||||
private currentCheckpoint: CrawlerCheckpoint = null;
|
private currentCheckpoint: ICrawlerCheckpoint = null;
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter {
|
||||||
const timeline = room.getLiveTimeline();
|
const timeline = room.getLiveTimeline();
|
||||||
const token = timeline.getPaginationToken("b");
|
const token = timeline.getPaginationToken("b");
|
||||||
|
|
||||||
const backCheckpoint: CrawlerCheckpoint = {
|
const backCheckpoint: ICrawlerCheckpoint = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
token: token,
|
token: token,
|
||||||
direction: "b",
|
direction: "b",
|
||||||
fullCrawl: true,
|
fullCrawl: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const forwardCheckpoint: CrawlerCheckpoint = {
|
const forwardCheckpoint: ICrawlerCheckpoint = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
token: token,
|
token: token,
|
||||||
direction: "f",
|
direction: "f",
|
||||||
|
@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* Search the event index using the given term for matching events.
|
* Search the event index using the given term for matching events.
|
||||||
*
|
*
|
||||||
* @param {SearchArgs} searchArgs The search configuration for the search,
|
* @param {ISearchArgs} searchArgs The search configuration for the search,
|
||||||
* sets the search term and determines the search result contents.
|
* sets the search term and determines the search result contents.
|
||||||
*
|
*
|
||||||
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
|
||||||
* of search results once the search is done.
|
* of search results once the search is done.
|
||||||
*/
|
*/
|
||||||
public async search(searchArgs: SearchArgs) {
|
public async search(searchArgs: ISearchArgs) {
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
return indexManager.searchEventIndex(searchArgs);
|
return indexManager.searchEventIndex(searchArgs);
|
||||||
}
|
}
|
||||||
|
@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
const indexManager = PlatformPeg.get().getEventIndexingManager();
|
||||||
|
|
||||||
const loadArgs: LoadArgs = {
|
const loadArgs: ILoadArgs = {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
};
|
};
|
||||||
|
|
|
@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
|
||||||
body.append('cross_signing_key', client.getCrossSigningId());
|
body.append('cross_signing_key', client.getCrossSigningId());
|
||||||
|
|
||||||
// add cross-signing status information
|
// add cross-signing status information
|
||||||
const crossSigning = client.crypto._crossSigningInfo;
|
const crossSigning = client.crypto.crossSigningInfo;
|
||||||
const secretStorage = client.crypto._secretStorage;
|
const secretStorage = client.crypto.secretStorage;
|
||||||
|
|
||||||
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
|
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
|
||||||
body.append("cross_signing_supported_by_hs",
|
body.append("cross_signing_supported_by_hs",
|
||||||
|
|
|
@ -36,6 +36,8 @@ import RoomViewStore from "./RoomViewStore";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
import { arrayHasDiff } from "../utils/arrays";
|
import { arrayHasDiff } from "../utils/arrays";
|
||||||
import { objectDiff } from "../utils/objects";
|
import { objectDiff } from "../utils/objects";
|
||||||
|
import { arrayHasOrderChange } from "../utils/arrays";
|
||||||
|
import { reorderLexicographically } from "../utils/stringOrderField";
|
||||||
|
|
||||||
type SpaceKey = string | symbol;
|
type SpaceKey = string | symbol;
|
||||||
|
|
||||||
|
@ -67,18 +69,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
|
||||||
}, [[], []]);
|
}, [[], []]);
|
||||||
};
|
};
|
||||||
|
|
||||||
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
|
const validOrder = (order: string): string | undefined => {
|
||||||
export const getOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
|
if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
|
||||||
let validatedOrder: string = null;
|
|
||||||
|
|
||||||
if (typeof order === "string" && Array.from(order).every((c: string) => {
|
|
||||||
const charCode = c.charCodeAt(0);
|
const charCode = c.charCodeAt(0);
|
||||||
return charCode >= 0x20 && charCode <= 0x7E;
|
return charCode >= 0x20 && charCode <= 0x7E;
|
||||||
})) {
|
})) {
|
||||||
validatedOrder = order;
|
return order;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return [validatedOrder, creationTs, roomId];
|
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
|
||||||
|
export const getChildOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
|
||||||
|
return [validOrder(order), creationTs, roomId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||||
|
@ -104,6 +106,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private _activeSpace?: Room = null;
|
private _activeSpace?: Room = null;
|
||||||
private _suggestedRooms: ISuggestedRoom[] = [];
|
private _suggestedRooms: ISuggestedRoom[] = [];
|
||||||
private _invitedSpaces = new Set<Room>();
|
private _invitedSpaces = new Set<Room>();
|
||||||
|
private spaceOrderLocalEchoMap = new Map<string, string>();
|
||||||
|
|
||||||
public get invitedSpaces(): Room[] {
|
public get invitedSpaces(): Room[] {
|
||||||
return Array.from(this._invitedSpaces);
|
return Array.from(this._invitedSpaces);
|
||||||
|
@ -223,7 +226,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const roomId = ev.getStateKey();
|
const roomId = ev.getStateKey();
|
||||||
const childRoom = this.matrixClient?.getRoom(roomId);
|
const childRoom = this.matrixClient?.getRoom(roomId);
|
||||||
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
|
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
|
||||||
return getOrder(ev.getContent().order, createTs, roomId);
|
return getChildOrder(ev.getContent().order, createTs, roomId);
|
||||||
}).map(ev => {
|
}).map(ev => {
|
||||||
return this.matrixClient.getRoom(ev.getStateKey());
|
return this.matrixClient.getRoom(ev.getStateKey());
|
||||||
}).filter(room => {
|
}).filter(room => {
|
||||||
|
@ -336,7 +339,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// });
|
// });
|
||||||
|
|
||||||
this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
|
this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
|
||||||
this.rootSpaces = rootSpaces;
|
this.rootSpaces = this.sortRootSpaces(rootSpaces);
|
||||||
this.parentMap = backrefs;
|
this.parentMap = backrefs;
|
||||||
|
|
||||||
// if the currently selected space no longer exists, remove its selection
|
// if the currently selected space no longer exists, remove its selection
|
||||||
|
@ -348,7 +351,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
|
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
|
||||||
|
|
||||||
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
|
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
|
||||||
this._invitedSpaces = new Set(invitedSpaces);
|
this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
|
||||||
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
|
||||||
}, 100, {trailing: true, leading: true});
|
}, 100, {trailing: true, leading: true});
|
||||||
|
|
||||||
|
@ -524,6 +527,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private notifyIfOrderChanged(): void {
|
||||||
|
const rootSpaces = this.sortRootSpaces(this.rootSpaces);
|
||||||
|
if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
|
||||||
|
this.rootSpaces = rootSpaces;
|
||||||
|
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onRoomState = (ev: MatrixEvent) => {
|
private onRoomState = (ev: MatrixEvent) => {
|
||||||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
@ -555,10 +566,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => {
|
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => {
|
||||||
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
|
if (!room.isSpaceRoom()) return;
|
||||||
|
|
||||||
|
if (ev.getType() === EventType.SpaceOrder) {
|
||||||
|
this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo
|
||||||
|
const order = ev.getContent()?.order;
|
||||||
|
const lastOrder = lastEv?.getContent()?.order;
|
||||||
|
if (order !== lastOrder) {
|
||||||
|
this.notifyIfOrderChanged();
|
||||||
|
}
|
||||||
|
} else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||||
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
||||||
const oldTags = lastEvent?.getContent()?.tags || {};
|
const oldTags = lastEv?.getContent()?.tags || {};
|
||||||
const newTags = ev.getContent()?.tags || {};
|
const newTags = ev.getContent()?.tags || {};
|
||||||
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
||||||
this.onRoomUpdate(room);
|
this.onRoomUpdate(room);
|
||||||
|
@ -600,9 +620,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
if (this.matrixClient) {
|
if (this.matrixClient) {
|
||||||
this.matrixClient.removeListener("Room", this.onRoom);
|
this.matrixClient.removeListener("Room", this.onRoom);
|
||||||
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
||||||
|
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
||||||
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
||||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||||
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
|
||||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
this.matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -613,9 +633,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
if (!SettingsStore.getValue("feature_spaces")) return;
|
if (!SettingsStore.getValue("feature_spaces")) return;
|
||||||
this.matrixClient.on("Room", this.onRoom);
|
this.matrixClient.on("Room", this.onRoom);
|
||||||
this.matrixClient.on("Room.myMembership", this.onRoom);
|
this.matrixClient.on("Room.myMembership", this.onRoom);
|
||||||
|
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
||||||
this.matrixClient.on("RoomState.events", this.onRoomState);
|
this.matrixClient.on("RoomState.events", this.onRoomState);
|
||||||
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
|
||||||
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
|
||||||
this.matrixClient.on("accountData", this.onAccountData);
|
this.matrixClient.on("accountData", this.onAccountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -700,6 +720,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
|
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSpaceTagOrdering = (space: Room): string | undefined => {
|
||||||
|
if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
|
||||||
|
return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
|
||||||
|
};
|
||||||
|
|
||||||
|
private sortRootSpaces(spaces: Room[]): Room[] {
|
||||||
|
return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setRootSpaceOrder(space: Room, order: string): Promise<void> {
|
||||||
|
this.spaceOrderLocalEchoMap.set(space.roomId, order);
|
||||||
|
try {
|
||||||
|
await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to set root space order", e);
|
||||||
|
if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) {
|
||||||
|
this.spaceOrderLocalEchoMap.delete(space.roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveRootSpace(fromIndex: number, toIndex: number): void {
|
||||||
|
const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering);
|
||||||
|
const changes = reorderLexicographically(currentOrders, fromIndex, toIndex);
|
||||||
|
|
||||||
|
changes.forEach(({ index, order }) => {
|
||||||
|
this.setRootSpaceOrder(this.rootSpaces[index], order);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.notifyIfOrderChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SpaceStore {
|
export default class SpaceStore {
|
||||||
|
|
|
@ -30,24 +30,24 @@ export default class EditorStateTransfer {
|
||||||
|
|
||||||
constructor(private readonly event: MatrixEvent) {}
|
constructor(private readonly event: MatrixEvent) {}
|
||||||
|
|
||||||
setEditorState(caret: Caret, serializedParts: SerializedPart[]) {
|
public setEditorState(caret: Caret, serializedParts: SerializedPart[]) {
|
||||||
this.caret = caret;
|
this.caret = caret;
|
||||||
this.serializedParts = serializedParts;
|
this.serializedParts = serializedParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasEditorState() {
|
public hasEditorState() {
|
||||||
return !!this.serializedParts;
|
return !!this.serializedParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSerializedParts() {
|
public getSerializedParts() {
|
||||||
return this.serializedParts;
|
return this.serializedParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCaret() {
|
public getCaret() {
|
||||||
return this.caret;
|
return this.caret;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEvent() {
|
public getEvent() {
|
||||||
return this.event;
|
return this.event;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
|
||||||
import { Action, DiffDOM, IDiff } from "diff-dom";
|
import { DiffDOM, IDiff } from "diff-dom";
|
||||||
import { IContent } from "matrix-js-sdk/src/models/event";
|
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
|
||||||
|
@ -149,7 +149,7 @@ function stringAsTextNode(string: string): Text {
|
||||||
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
|
||||||
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
|
||||||
switch (diff.action) {
|
switch (diff.action) {
|
||||||
case Action.ReplaceElement: {
|
case "replaceElement": {
|
||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
|
const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue));
|
||||||
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
|
const insNode = wrapInsertion(diffTreeToDOM(diff.newValue));
|
||||||
|
@ -158,17 +158,17 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||||
refNode.parentNode.replaceChild(container, refNode);
|
refNode.parentNode.replaceChild(container, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.RemoveTextElement: {
|
case "removeTextElement": {
|
||||||
const delNode = wrapDeletion(stringAsTextNode(diff.value));
|
const delNode = wrapDeletion(stringAsTextNode(diff.value));
|
||||||
refNode.parentNode.replaceChild(delNode, refNode);
|
refNode.parentNode.replaceChild(delNode, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.RemoveElement: {
|
case "removeElement": {
|
||||||
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
|
const delNode = wrapDeletion(diffTreeToDOM(diff.element));
|
||||||
refNode.parentNode.replaceChild(delNode, refNode);
|
refNode.parentNode.replaceChild(delNode, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.ModifyTextElement: {
|
case "modifyTextElement": {
|
||||||
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
|
const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue);
|
||||||
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
diffMathPatch.diff_cleanupSemantic(textDiffs);
|
||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
|
@ -184,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||||
refNode.parentNode.replaceChild(container, refNode);
|
refNode.parentNode.replaceChild(container, refNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.AddElement: {
|
case "addElement": {
|
||||||
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
|
const insNode = wrapInsertion(diffTreeToDOM(diff.element));
|
||||||
insertBefore(refParentNode, refNode, insNode);
|
insertBefore(refParentNode, refNode, insNode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Action.AddTextElement: {
|
case "addTextElement": {
|
||||||
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
|
// XXX: sometimes diffDOM says insert a newline when there shouldn't be one
|
||||||
// but we must insert the node anyway so that we don't break the route child IDs.
|
// but we must insert the node anyway so that we don't break the route child IDs.
|
||||||
// See https://github.com/fiduswriter/diffDOM/issues/100
|
// See https://github.com/fiduswriter/diffDOM/issues/100
|
||||||
|
@ -199,9 +199,9 @@ function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatc
|
||||||
}
|
}
|
||||||
// e.g. when changing a the href of a link,
|
// e.g. when changing a the href of a link,
|
||||||
// show the link with old href as removed and with the new href as added
|
// show the link with old href as removed and with the new href as added
|
||||||
case Action.RemoveAttribute:
|
case "removeAttribute":
|
||||||
case Action.AddAttribute:
|
case "addAttribute":
|
||||||
case Action.ModifyAttribute: {
|
case "modifyAttribute": {
|
||||||
const delNode = wrapDeletion(refNode.cloneNode(true));
|
const delNode = wrapDeletion(refNode.cloneNode(true));
|
||||||
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
const updatedNode = refNode.cloneNode(true) as HTMLElement;
|
||||||
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,23 +14,51 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
import {getAddressType} from '../UserAddress';
|
|
||||||
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
import { AddressType, getAddressType } from '../UserAddress';
|
||||||
import GroupStore from '../stores/GroupStore';
|
import GroupStore from '../stores/GroupStore';
|
||||||
import {_t} from "../languageHandler";
|
import { _t } from "../languageHandler";
|
||||||
import * as sdk from "../index";
|
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import {defer} from "./promise";
|
import { defer, IDeferred } from "./promise";
|
||||||
|
import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
|
||||||
|
|
||||||
|
export enum InviteState {
|
||||||
|
Invited = "invited",
|
||||||
|
Error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IError {
|
||||||
|
errorText: string;
|
||||||
|
errcode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
||||||
|
|
||||||
|
export type CompletionStates = Record<string, InviteState>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||||
*/
|
*/
|
||||||
export default class MultiInviter {
|
export default class MultiInviter {
|
||||||
|
private readonly roomId?: string;
|
||||||
|
private readonly groupId?: string;
|
||||||
|
|
||||||
|
private canceled = false;
|
||||||
|
private addresses: string[] = [];
|
||||||
|
private busy = false;
|
||||||
|
private _fatal = false;
|
||||||
|
private completionStates: CompletionStates = {}; // State of each address (invited or error)
|
||||||
|
private errors: Record<string, IError> = {}; // { address: {errorText, errcode} }
|
||||||
|
private deferred: IDeferred<CompletionStates> = null;
|
||||||
|
private reason: string = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} targetId The ID of the room or group to invite to
|
* @param {string} targetId The ID of the room or group to invite to
|
||||||
*/
|
*/
|
||||||
constructor(targetId) {
|
constructor(targetId: string) {
|
||||||
if (targetId[0] === '+') {
|
if (targetId[0] === '+') {
|
||||||
this.roomId = null;
|
this.roomId = null;
|
||||||
this.groupId = targetId;
|
this.groupId = targetId;
|
||||||
|
@ -39,41 +66,38 @@ export default class MultiInviter {
|
||||||
this.roomId = targetId;
|
this.roomId = targetId;
|
||||||
this.groupId = null;
|
this.groupId = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.canceled = false;
|
public get fatal() {
|
||||||
this.addrs = [];
|
return this._fatal;
|
||||||
this.busy = false;
|
|
||||||
this.completionStates = {}; // State of each address (invited or error)
|
|
||||||
this.errors = {}; // { address: {errorText, errcode} }
|
|
||||||
this.deferred = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite users to this room. This may only be called once per
|
* Invite users to this room. This may only be called once per
|
||||||
* instance of the class.
|
* instance of the class.
|
||||||
*
|
*
|
||||||
* @param {array} addrs Array of addresses to invite
|
* @param {array} addresses Array of addresses to invite
|
||||||
* @param {string} reason Reason for inviting (optional)
|
* @param {string} reason Reason for inviting (optional)
|
||||||
* @returns {Promise} Resolved when all invitations in the queue are complete
|
* @returns {Promise} Resolved when all invitations in the queue are complete
|
||||||
*/
|
*/
|
||||||
invite(addrs, reason) {
|
public invite(addresses, reason?: string): Promise<CompletionStates> {
|
||||||
if (this.addrs.length > 0) {
|
if (this.addresses.length > 0) {
|
||||||
throw new Error("Already inviting/invited");
|
throw new Error("Already inviting/invited");
|
||||||
}
|
}
|
||||||
this.addrs.push(...addrs);
|
this.addresses.push(...addresses);
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
|
|
||||||
for (const addr of this.addrs) {
|
for (const addr of this.addresses) {
|
||||||
if (getAddressType(addr) === null) {
|
if (getAddressType(addr) === null) {
|
||||||
this.completionStates[addr] = 'error';
|
this.completionStates[addr] = InviteState.Error;
|
||||||
this.errors[addr] = {
|
this.errors[addr] = {
|
||||||
errcode: 'M_INVALID',
|
errcode: 'M_INVALID',
|
||||||
errorText: _t('Unrecognised address'),
|
errorText: _t('Unrecognised address'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.deferred = defer();
|
this.deferred = defer<CompletionStates>();
|
||||||
this._inviteMore(0);
|
this.inviteMore(0);
|
||||||
|
|
||||||
return this.deferred.promise;
|
return this.deferred.promise;
|
||||||
}
|
}
|
||||||
|
@ -81,33 +105,36 @@ export default class MultiInviter {
|
||||||
/**
|
/**
|
||||||
* Stops inviting. Causes promises returned by invite() to be rejected.
|
* Stops inviting. Causes promises returned by invite() to be rejected.
|
||||||
*/
|
*/
|
||||||
cancel() {
|
public cancel(): void {
|
||||||
if (!this.busy) return;
|
if (!this.busy) return;
|
||||||
|
|
||||||
this._canceled = true;
|
this.canceled = true;
|
||||||
this.deferred.reject(new Error('canceled'));
|
this.deferred.reject(new Error('canceled'));
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletionState(addr) {
|
public getCompletionState(addr: string): InviteState {
|
||||||
return this.completionStates[addr];
|
return this.completionStates[addr];
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorText(addr) {
|
public getErrorText(addr: string): string {
|
||||||
return this.errors[addr] ? this.errors[addr].errorText : null;
|
return this.errors[addr] ? this.errors[addr].errorText : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _inviteToRoom(roomId, addr, ignoreProfile) {
|
private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> {
|
||||||
const addrType = getAddressType(addr);
|
const addrType = getAddressType(addr);
|
||||||
|
|
||||||
if (addrType === 'email') {
|
if (addrType === AddressType.Email) {
|
||||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||||
} else if (addrType === 'mx-user-id') {
|
} else if (addrType === AddressType.MatrixUserId) {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
if (!room) throw new Error("Room not found");
|
if (!room) throw new Error("Room not found");
|
||||||
|
|
||||||
const member = room.getMember(addr);
|
const member = room.getMember(addr);
|
||||||
if (member && ['join', 'invite'].includes(member.membership)) {
|
if (member && ['join', 'invite'].includes(member.membership)) {
|
||||||
throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"};
|
throw new new MatrixError({
|
||||||
|
errcode: "RIOT.ALREADY_IN_ROOM",
|
||||||
|
error: "Member already invited",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
|
||||||
|
@ -124,28 +151,28 @@ export default class MultiInviter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_doInvite(address, ignoreProfile) {
|
private doInvite(address: string, ignoreProfile = false): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
console.log(`Inviting ${address}`);
|
console.log(`Inviting ${address}`);
|
||||||
|
|
||||||
let doInvite;
|
let doInvite;
|
||||||
if (this.groupId !== null) {
|
if (this.groupId !== null) {
|
||||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||||
} else {
|
} else {
|
||||||
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
|
doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
doInvite.then(() => {
|
doInvite.then(() => {
|
||||||
if (this._canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.completionStates[address] = 'invited';
|
this.completionStates[address] = InviteState.Invited;
|
||||||
delete this.errors[address];
|
delete this.errors[address];
|
||||||
|
|
||||||
resolve();
|
resolve();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (this._canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +188,7 @@ export default class MultiInviter {
|
||||||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||||
// we're being throttled so wait a bit & try again
|
// we're being throttled so wait a bit & try again
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this._doInvite(address, ignoreProfile).then(resolve, reject);
|
this.doInvite(address, ignoreProfile).then(resolve, reject);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
return;
|
return;
|
||||||
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
|
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
|
||||||
|
@ -171,7 +198,7 @@ export default class MultiInviter {
|
||||||
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
||||||
// Invite without the profile check
|
// Invite without the profile check
|
||||||
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
||||||
this._doInvite(address, true).then(resolve, reject);
|
this.doInvite(address, true).then(resolve, reject);
|
||||||
} else if (err.errcode === "M_BAD_STATE") {
|
} else if (err.errcode === "M_BAD_STATE") {
|
||||||
errorText = _t("The user must be unbanned before they can be invited.");
|
errorText = _t("The user must be unbanned before they can be invited.");
|
||||||
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
|
||||||
|
@ -180,14 +207,14 @@ export default class MultiInviter {
|
||||||
errorText = _t('Unknown server error');
|
errorText = _t('Unknown server error');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.completionStates[address] = 'error';
|
this.completionStates[address] = InviteState.Error;
|
||||||
this.errors[address] = {errorText, errcode: err.errcode};
|
this.errors[address] = { errorText, errcode: err.errcode };
|
||||||
|
|
||||||
this.busy = !fatal;
|
this.busy = !fatal;
|
||||||
this.fatal = fatal;
|
this._fatal = fatal;
|
||||||
|
|
||||||
if (fatal) {
|
if (fatal) {
|
||||||
reject();
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
|
@ -195,22 +222,22 @@ export default class MultiInviter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_inviteMore(nextIndex, ignoreProfile) {
|
private inviteMore(nextIndex: number, ignoreProfile = false): void {
|
||||||
if (this._canceled) {
|
if (this.canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex === this.addrs.length) {
|
if (nextIndex === this.addresses.length) {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||||
// There were problems inviting some people - see if we can invite them
|
// There were problems inviting some people - see if we can invite them
|
||||||
// without caring if they exist or not.
|
// without caring if they exist or not.
|
||||||
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
|
const unknownProfileUsers = Object.keys(this.errors)
|
||||||
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
|
.filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode));
|
||||||
|
|
||||||
if (unknownProfileUsers.length > 0) {
|
if (unknownProfileUsers.length > 0) {
|
||||||
const inviteUnknowns = () => {
|
const inviteUnknowns = () => {
|
||||||
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
|
const promises = unknownProfileUsers.map(u => this.doInvite(u, true));
|
||||||
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,15 +246,17 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
|
||||||
console.log("Showing failed to invite dialog...");
|
console.log("Showing failed to invite dialog...");
|
||||||
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
|
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
|
||||||
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
|
unknownProfileUsers: unknownProfileUsers.map(u => ({
|
||||||
|
userId: u,
|
||||||
|
errorText: this.errors[u].errorText,
|
||||||
|
})),
|
||||||
onInviteAnyways: () => inviteUnknowns(),
|
onInviteAnyways: () => inviteUnknowns(),
|
||||||
onGiveUp: () => {
|
onGiveUp: () => {
|
||||||
// Fake all the completion states because we already warned the user
|
// Fake all the completion states because we already warned the user
|
||||||
for (const addr of unknownProfileUsers) {
|
for (const addr of unknownProfileUsers) {
|
||||||
this.completionStates[addr] = 'invited';
|
this.completionStates[addr] = InviteState.Invited;
|
||||||
}
|
}
|
||||||
this.deferred.resolve(this.completionStates);
|
this.deferred.resolve(this.completionStates);
|
||||||
},
|
},
|
||||||
|
@ -239,25 +268,25 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addr = this.addrs[nextIndex];
|
const addr = this.addresses[nextIndex];
|
||||||
|
|
||||||
// don't try to invite it if it's an invalid address
|
// don't try to invite it if it's an invalid address
|
||||||
// (it will already be marked as an error though,
|
// (it will already be marked as an error though,
|
||||||
// so no need to do so again)
|
// so no need to do so again)
|
||||||
if (getAddressType(addr) === null) {
|
if (getAddressType(addr) === null) {
|
||||||
this._inviteMore(nextIndex + 1);
|
this.inviteMore(nextIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't re-invite (there's no way in the UI to do this, but
|
// don't re-invite (there's no way in the UI to do this, but
|
||||||
// for sanity's sake)
|
// for sanity's sake)
|
||||||
if (this.completionStates[addr] === 'invited') {
|
if (this.completionStates[addr] === InviteState.Invited) {
|
||||||
this._inviteMore(nextIndex + 1);
|
this.inviteMore(nextIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._doInvite(addr, ignoreProfile).then(() => {
|
this.doInvite(addr, ignoreProfile).then(() => {
|
||||||
this._inviteMore(nextIndex + 1, ignoreProfile);
|
this.inviteMore(nextIndex + 1, ignoreProfile);
|
||||||
}).catch(() => this.deferred.resolve(this.completionStates));
|
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {percentageOf, percentageWithin} from "./numbers";
|
import { percentageOf, percentageWithin } from "./numbers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quickly resample an array to have less/more data points. If an input which is larger
|
* Quickly resample an array to have less/more data points. If an input which is larger
|
||||||
|
@ -223,6 +223,21 @@ export function arrayMerge<T>(...a: T[][]): T[] {
|
||||||
}, new Set<T>()));
|
}, new Set<T>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a single element from fromIndex to toIndex.
|
||||||
|
* @param {array} list the list from which to construct the new list.
|
||||||
|
* @param {number} fromIndex the index of the element to move.
|
||||||
|
* @param {number} toIndex the index of where to put the element.
|
||||||
|
* @returns {array} A new array with the requested value moved.
|
||||||
|
*/
|
||||||
|
export function moveElement<T>(list: T[], fromIndex: number, toIndex: number): T[] {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(fromIndex, 1);
|
||||||
|
result.splice(toIndex, 0, removed);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper functions to perform LINQ-like queries on arrays.
|
* Helper functions to perform LINQ-like queries on arrays.
|
||||||
*/
|
*/
|
||||||
|
|
148
src/utils/stringOrderField.ts
Normal file
148
src/utils/stringOrderField.ts
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import { moveElement } from "./arrays";
|
||||||
|
|
||||||
|
export function midPointsBetweenStrings(
|
||||||
|
a: string,
|
||||||
|
b: string,
|
||||||
|
count: number,
|
||||||
|
maxLen: number,
|
||||||
|
alphabet = DEFAULT_ALPHABET,
|
||||||
|
): string[] {
|
||||||
|
const padN = Math.min(Math.max(a.length, b.length), maxLen);
|
||||||
|
const padA = alphabetPad(a, padN, alphabet);
|
||||||
|
const padB = alphabetPad(b, padN, alphabet);
|
||||||
|
const baseA = stringToBase(padA, alphabet);
|
||||||
|
const baseB = stringToBase(padB, alphabet);
|
||||||
|
|
||||||
|
if (baseB - baseA - BigInt(1) < count) {
|
||||||
|
if (padN < maxLen) {
|
||||||
|
// this recurses once at most due to the new limit of n+1
|
||||||
|
return midPointsBetweenStrings(
|
||||||
|
alphabetPad(padA, padN + 1, alphabet),
|
||||||
|
alphabetPad(padB, padN + 1, alphabet),
|
||||||
|
count,
|
||||||
|
padN + 1,
|
||||||
|
alphabet,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = (baseB - baseA) / BigInt(count + 1);
|
||||||
|
const start = BigInt(baseA + step);
|
||||||
|
return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEntry {
|
||||||
|
index: number;
|
||||||
|
order: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reorderLexicographically = (
|
||||||
|
orders: Array<string | undefined>,
|
||||||
|
fromIndex: number,
|
||||||
|
toIndex: number,
|
||||||
|
maxLen = 50,
|
||||||
|
): IEntry[] => {
|
||||||
|
// sanity check inputs
|
||||||
|
if (
|
||||||
|
fromIndex < 0 || toIndex < 0 ||
|
||||||
|
fromIndex > orders.length || toIndex > orders.length ||
|
||||||
|
fromIndex === toIndex
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// zip orders with their indices to simplify later index wrangling
|
||||||
|
const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order }));
|
||||||
|
// apply the fundamental order update to the zipped array
|
||||||
|
const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex);
|
||||||
|
|
||||||
|
// check if we have to fill undefined orders to complete placement
|
||||||
|
const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined;
|
||||||
|
|
||||||
|
let leftBoundIdx = toIndex;
|
||||||
|
let rightBoundIdx = toIndex;
|
||||||
|
|
||||||
|
let canMoveLeft = true;
|
||||||
|
const nextBase = newOrder[toIndex + 1]?.order !== undefined
|
||||||
|
? stringToBase(newOrder[toIndex + 1].order)
|
||||||
|
: BigInt(Number.MAX_VALUE);
|
||||||
|
|
||||||
|
// check how far left we would have to mutate to fit in that direction
|
||||||
|
for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) {
|
||||||
|
if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break;
|
||||||
|
leftBoundIdx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the left move would be sufficient
|
||||||
|
const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order);
|
||||||
|
const bigToIndex = BigInt(toIndex);
|
||||||
|
if (leftBoundIdx === 0 &&
|
||||||
|
firstOrderBase !== undefined &&
|
||||||
|
nextBase - firstOrderBase <= bigToIndex &&
|
||||||
|
firstOrderBase <= bigToIndex
|
||||||
|
) {
|
||||||
|
canMoveLeft = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDisplaceRight = !orderToLeftUndefined;
|
||||||
|
let canMoveRight = canDisplaceRight;
|
||||||
|
if (canDisplaceRight) {
|
||||||
|
const prevBase = newOrder[toIndex - 1]?.order !== undefined
|
||||||
|
? stringToBase(newOrder[toIndex - 1]?.order)
|
||||||
|
: BigInt(Number.MIN_VALUE);
|
||||||
|
|
||||||
|
// check how far right we would have to mutate to fit in that direction
|
||||||
|
for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) {
|
||||||
|
if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break;
|
||||||
|
rightBoundIdx = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the right move would be sufficient
|
||||||
|
if (rightBoundIdx === newOrder.length - 1 &&
|
||||||
|
(newOrder[rightBoundIdx]
|
||||||
|
? stringToBase(newOrder[rightBoundIdx].order)
|
||||||
|
: BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex)
|
||||||
|
) {
|
||||||
|
canMoveRight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick the cheaper direction
|
||||||
|
const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER;
|
||||||
|
const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER;
|
||||||
|
if (orderToLeftUndefined || leftDiff < rightDiff) {
|
||||||
|
rightBoundIdx = toIndex;
|
||||||
|
} else {
|
||||||
|
leftBoundIdx = toIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? "";
|
||||||
|
const nextOrder = newOrder[rightBoundIdx + 1]?.order
|
||||||
|
?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1);
|
||||||
|
|
||||||
|
const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen);
|
||||||
|
|
||||||
|
return changes.map((order, i) => ({
|
||||||
|
index: newOrder[leftBoundIdx + i].index,
|
||||||
|
order,
|
||||||
|
}));
|
||||||
|
};
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import * as Recorder from 'opus-recorder';
|
import * as Recorder from 'opus-recorder';
|
||||||
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
|
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
import CallMediaHandler from "../CallMediaHandler";
|
import MediaDeviceHandler from "../MediaDeviceHandler";
|
||||||
import {SimpleObservable} from "matrix-widget-api";
|
import {SimpleObservable} from "matrix-widget-api";
|
||||||
import {clamp, percentageOf, percentageWithin} from "../utils/numbers";
|
import {clamp, percentageOf, percentageWithin} from "../utils/numbers";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
@ -97,7 +97,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
||||||
audio: {
|
audio: {
|
||||||
channelCount: CHANNELS,
|
channelCount: CHANNELS,
|
||||||
noiseSuppression: true, // browsers ignore constraints they can't honour
|
noiseSuppression: true, // browsers ignore constraints they can't honour
|
||||||
deviceId: CallMediaHandler.getAudioInput(),
|
deviceId: MediaDeviceHandler.getAudioInput(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.recorderContext = createAudioContext({
|
this.recorderContext = createAudioContext({
|
||||||
|
|
|
@ -1,21 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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 React from 'react';
|
||||||
import ReactTestUtils from 'react-dom/test-utils';
|
import ReactTestUtils from 'react-dom/test-utils';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import * as TestUtils from '../../../test-utils';
|
import * as TestUtils from '../../../test-utils';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
|
||||||
import sdk from '../../../skinned-sdk';
|
import sdk from '../../../skinned-sdk';
|
||||||
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
import {Room, RoomMember, User} from 'matrix-js-sdk';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { compare } from "../../../../src/utils/strings";
|
import { compare } from "../../../../src/utils/strings";
|
||||||
|
import MemberList from "../../../../src/components/views/rooms/MemberList";
|
||||||
|
|
||||||
function generateRoomId() {
|
function generateRoomId() {
|
||||||
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
return '!' + Math.random().toString().slice(2, 10) + ':domain';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe('MemberList', () => {
|
describe('MemberList', () => {
|
||||||
function createRoom(opts) {
|
function createRoom(opts) {
|
||||||
const room = new Room(generateRoomId(), null, client.getUserId());
|
const room = new Room(generateRoomId(), null, client.getUserId());
|
||||||
|
@ -97,13 +112,19 @@ describe('MemberList', () => {
|
||||||
memberListRoom.currentState.members[member.userId] = member;
|
memberListRoom.currentState.members[member.userId] = member;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MemberList = sdk.getComponent('views.rooms.MemberList');
|
|
||||||
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
|
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
|
||||||
const gatherWrappedRef = (r) => {
|
const gatherWrappedRef = (r) => {
|
||||||
memberList = r;
|
memberList = r;
|
||||||
};
|
};
|
||||||
root = ReactDOM.render(<WrappedMemberList roomId={memberListRoom.roomId}
|
root = ReactDOM.render(
|
||||||
wrappedRef={gatherWrappedRef} />, parentDiv);
|
(
|
||||||
|
<WrappedMemberList
|
||||||
|
roomId={memberListRoom.roomId}
|
||||||
|
wrappedRef={gatherWrappedRef}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
parentDiv,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach((done) => {
|
afterEach((done) => {
|
||||||
|
@ -213,8 +234,8 @@ describe('MemberList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList.showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||||
|
@ -225,7 +246,7 @@ describe('MemberList', () => {
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList._showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||||
|
@ -254,8 +275,8 @@ describe('MemberList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList.showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
||||||
|
@ -273,8 +294,8 @@ describe('MemberList', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bypass all the event listeners and skip to the good part
|
// Bypass all the event listeners and skip to the good part
|
||||||
memberList._showPresence = enablePresence;
|
memberList.showPresence = enablePresence;
|
||||||
memberList._updateListNow();
|
memberList.updateListNow();
|
||||||
|
|
||||||
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
|
||||||
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
|
|
@ -6,7 +6,6 @@ import * as TestUtils from '../../../test-utils';
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
|
||||||
import sdk from '../../../skinned-sdk';
|
import sdk from '../../../skinned-sdk';
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
import dis from '../../../../src/dispatcher/dispatcher';
|
import dis from '../../../../src/dispatcher/dispatcher';
|
||||||
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
||||||
|
@ -68,9 +67,7 @@ describe('RoomList', () => {
|
||||||
const RoomList = sdk.getComponent('views.rooms.RoomList');
|
const RoomList = sdk.getComponent('views.rooms.RoomList');
|
||||||
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
|
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
|
||||||
root = ReactDOM.render(
|
root = ReactDOM.render(
|
||||||
<DragDropContext>
|
<WrappedRoomList searchFilter="" onResize={() => {}} />,
|
||||||
<WrappedRoomList searchFilter="" onResize={() => {}} />
|
|
||||||
</DragDropContext>,
|
|
||||||
parentDiv,
|
parentDiv,
|
||||||
);
|
);
|
||||||
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
|
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
|
||||||
|
|
|
@ -140,8 +140,6 @@ async function changeRoomSettings(session, settings) {
|
||||||
|
|
||||||
if (settings.alias) {
|
if (settings.alias) {
|
||||||
session.log.step(`sets alias to ${settings.alias}`);
|
session.log.step(`sets alias to ${settings.alias}`);
|
||||||
const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
|
|
||||||
await summary.click();
|
|
||||||
const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
|
const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
|
||||||
await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
|
await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
|
||||||
const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
|
const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
|
||||||
|
|
291
test/utils/stringOrderField-test.ts
Normal file
291
test/utils/stringOrderField-test.ts
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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 { sortBy } from "lodash";
|
||||||
|
import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
|
import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField";
|
||||||
|
|
||||||
|
const moveLexicographicallyTest = (
|
||||||
|
orders: Array<string | undefined>,
|
||||||
|
fromIndex: number,
|
||||||
|
toIndex: number,
|
||||||
|
expectedChanges: number,
|
||||||
|
maxLength?: number,
|
||||||
|
): void => {
|
||||||
|
const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength);
|
||||||
|
|
||||||
|
const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]);
|
||||||
|
ops.forEach(({ index, order }) => {
|
||||||
|
zipped[index][1] = order;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newOrders = sortBy(zipped, i => i[1]);
|
||||||
|
expect(newOrders[toIndex][0]).toBe(fromIndex);
|
||||||
|
expect(ops).toHaveLength(expectedChanges);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("stringOrderField", () => {
|
||||||
|
describe("midPointsBetweenStrings", () => {
|
||||||
|
it("should work", () => {
|
||||||
|
expect(averageBetweenStrings("!!", "##")).toBe('""');
|
||||||
|
const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort();
|
||||||
|
expect(midpoints[0]).toBe("a");
|
||||||
|
expect(midpoints[4]).toBe("e");
|
||||||
|
expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when the request is not possible", () => {
|
||||||
|
expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]);
|
||||||
|
expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reorderLexicographically", () => {
|
||||||
|
it("should work when moving left", () => {
|
||||||
|
moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work when moving right", () => {
|
||||||
|
moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work when all orders are undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined, undefined, undefined],
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work when moving to end and all orders are undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined, undefined, undefined],
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work when moving left and some orders are undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["a", "c", "e", undefined, undefined, undefined],
|
||||||
|
5,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["a", "a", "e", undefined, undefined, undefined],
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving to the start when all is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined],
|
||||||
|
2,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving to the end when all is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined],
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving left when all is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined, undefined, undefined],
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving right when all is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined],
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving more right when all is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined],
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving left when right is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["20", undefined, undefined, undefined, undefined, undefined],
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving right when right is undefined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined],
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving left when right is defined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["10", "20", "30", "40", undefined, undefined],
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving right when right is defined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["10", "20", "30", "40", "50", undefined],
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving left when all is defined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["11", "13", "15", "17", "19"],
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving right when all is defined", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["11", "13", "15", "17", "19"],
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving left into no left space", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["11", "12", "13", "14", "19"],
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[
|
||||||
|
DEFAULT_ALPHABET.charAt(0),
|
||||||
|
// Target
|
||||||
|
DEFAULT_ALPHABET.charAt(1),
|
||||||
|
DEFAULT_ALPHABET.charAt(2),
|
||||||
|
DEFAULT_ALPHABET.charAt(3),
|
||||||
|
DEFAULT_ALPHABET.charAt(4),
|
||||||
|
DEFAULT_ALPHABET.charAt(5),
|
||||||
|
],
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving right into no right space", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["15", "16", "17", "18", "19"],
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving right into no left space", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["11", "12", "13", "14", "15", "16", undefined],
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
);
|
||||||
|
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["0", "1", "2", "3", "4", "5"],
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work moving left into no right space", () => {
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
["15", "16", "17", "18", "19"],
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
|
||||||
|
moveLexicographicallyTest(
|
||||||
|
[
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
|
||||||
|
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
|
||||||
|
],
|
||||||
|
4,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
233
yarn.lock
233
yarn.lock
|
@ -1017,13 +1017,20 @@
|
||||||
pirates "^4.0.0"
|
pirates "^4.0.0"
|
||||||
source-map-support "^0.5.16"
|
source-map-support "^0.5.16"
|
||||||
|
|
||||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
|
||||||
version "7.12.5"
|
version "7.12.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
|
||||||
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
|
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
|
||||||
|
version "7.14.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
|
||||||
|
integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3":
|
"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3":
|
||||||
version "7.12.7"
|
version "7.12.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
|
||||||
|
@ -1327,6 +1334,7 @@
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
|
||||||
version "3.2.3"
|
version "3.2.3"
|
||||||
|
uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
|
||||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
|
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
|
||||||
|
|
||||||
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
|
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
|
||||||
|
@ -1509,6 +1517,14 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/hoist-non-react-statics@^3.3.0":
|
||||||
|
version "3.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||||
|
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
||||||
|
@ -1625,12 +1641,29 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/react-dom@^16.9.10":
|
"@types/react-beautiful-dnd@^13.0.0":
|
||||||
version "16.9.10"
|
version "13.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f"
|
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
|
||||||
integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==
|
integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "^16"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-dom@^17.0.2":
|
||||||
|
version "17.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc"
|
||||||
|
integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-redux@^7.1.16":
|
||||||
|
version "7.1.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
|
||||||
|
integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
|
||||||
|
dependencies:
|
||||||
|
"@types/hoist-non-react-statics" "^3.3.0"
|
||||||
|
"@types/react" "*"
|
||||||
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
redux "^4.0.0"
|
||||||
|
|
||||||
"@types/react-transition-group@^4.4.0":
|
"@types/react-transition-group@^4.4.0":
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
|
@ -1639,12 +1672,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9":
|
"@types/react@*", "@types/react@^17.0.2":
|
||||||
version "16.14.2"
|
version "17.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
|
||||||
integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ==
|
integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/sanitize-html@^2.3.1":
|
"@types/sanitize-html@^2.3.1":
|
||||||
|
@ -1654,6 +1688,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
htmlparser2 "^6.0.0"
|
htmlparser2 "^6.0.0"
|
||||||
|
|
||||||
|
"@types/scheduler@*":
|
||||||
|
version "0.16.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
||||||
|
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
||||||
|
|
||||||
"@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"
|
||||||
|
@ -2121,14 +2160,6 @@ babel-preset-jest@^26.6.2:
|
||||||
babel-plugin-jest-hoist "^26.6.2"
|
babel-plugin-jest-hoist "^26.6.2"
|
||||||
babel-preset-current-node-syntax "^1.0.0"
|
babel-preset-current-node-syntax "^1.0.0"
|
||||||
|
|
||||||
babel-runtime@^6.26.0:
|
|
||||||
version "6.26.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
|
||||||
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
|
|
||||||
dependencies:
|
|
||||||
core-js "^2.4.0"
|
|
||||||
regenerator-runtime "^0.11.0"
|
|
||||||
|
|
||||||
bail@^1.0.0:
|
bail@^1.0.0:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
|
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
|
||||||
|
@ -2647,11 +2678,6 @@ core-js@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||||
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
|
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
|
||||||
|
|
||||||
core-js@^2.4.0:
|
|
||||||
version "2.6.12"
|
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
|
||||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
|
||||||
|
|
||||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||||
|
@ -2711,6 +2737,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
|
css-box-model@^1.2.0:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
|
||||||
|
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
|
||||||
|
dependencies:
|
||||||
|
tiny-invariant "^1.0.6"
|
||||||
|
|
||||||
css-select@^4.1.2:
|
css-select@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286"
|
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286"
|
||||||
|
@ -4240,7 +4273,7 @@ highlight.js@^10.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f"
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f"
|
||||||
integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==
|
integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==
|
||||||
|
|
||||||
hoist-non-react-statics@^3.3.0:
|
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||||
|
@ -4455,13 +4488,6 @@ internal-slot@^1.0.2:
|
||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.2"
|
side-channel "^1.0.2"
|
||||||
|
|
||||||
invariant@^2.2.2, invariant@^2.2.4:
|
|
||||||
version "2.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
|
||||||
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.0.0"
|
|
||||||
|
|
||||||
ip-regex@^2.1.0:
|
ip-regex@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
||||||
|
@ -5599,11 +5625,6 @@ locate-path@^5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
lodash-es@^4.2.1:
|
|
||||||
version "4.17.20"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
|
|
||||||
integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
|
|
||||||
|
|
||||||
lodash.escape@^4.0.1:
|
lodash.escape@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
|
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
|
||||||
|
@ -5624,7 +5645,7 @@ lodash.sortby@^4.7.0:
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
|
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
|
||||||
version "4.17.21"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
@ -5752,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
|
||||||
"@babel/traverse" "^7.13.17"
|
"@babel/traverse" "^7.13.17"
|
||||||
walk "^2.3.14"
|
walk "^2.3.14"
|
||||||
|
|
||||||
matrix-widget-api@^0.1.0-beta.14:
|
matrix-widget-api@^0.1.0-beta.15:
|
||||||
version "0.1.0-beta.14"
|
version "0.1.0-beta.15"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
|
||||||
integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
|
integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
@ -5793,10 +5814,10 @@ mdurl@~1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
|
||||||
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
|
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
|
||||||
|
|
||||||
memoize-one@^3.0.1:
|
memoize-one@^5.1.1:
|
||||||
version "3.1.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
|
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||||
integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA==
|
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||||
|
|
||||||
meow@^9.0.0:
|
meow@^9.0.0:
|
||||||
version "9.0.0"
|
version "9.0.0"
|
||||||
|
@ -6442,11 +6463,6 @@ path-type@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||||
|
|
||||||
performance-now@^0.2.0:
|
|
||||||
version "0.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
|
||||||
integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
|
|
||||||
|
|
||||||
performance-now@^2.1.0:
|
performance-now@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
|
@ -6656,7 +6672,7 @@ prompts@^2.0.1:
|
||||||
kleur "^3.0.3"
|
kleur "^3.0.3"
|
||||||
sisteransi "^1.0.5"
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
|
prop-types@^15.6.2, prop-types@^15.7.0, 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==
|
||||||
|
@ -6733,12 +6749,12 @@ quick-lru@^4.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
||||||
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
|
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
|
||||||
|
|
||||||
raf-schd@^2.1.0:
|
raf-schd@^4.0.2:
|
||||||
version "2.1.2"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62"
|
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||||
integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g==
|
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
||||||
|
|
||||||
raf@^3.1.0, raf@^3.4.1:
|
raf@^3.4.1:
|
||||||
version "3.4.1"
|
version "3.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||||
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
|
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
|
||||||
|
@ -6765,21 +6781,18 @@ re-resizable@^6.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-memoize "^2.5.1"
|
fast-memoize "^2.5.1"
|
||||||
|
|
||||||
react-beautiful-dnd@^4.0.1:
|
react-beautiful-dnd@^13.1.0:
|
||||||
version "4.0.1"
|
version "13.1.0"
|
||||||
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-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
|
||||||
integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA==
|
integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime "^6.26.0"
|
"@babel/runtime" "^7.9.2"
|
||||||
invariant "^2.2.2"
|
css-box-model "^1.2.0"
|
||||||
memoize-one "^3.0.1"
|
memoize-one "^5.1.1"
|
||||||
prop-types "^15.6.0"
|
raf-schd "^4.0.2"
|
||||||
raf-schd "^2.1.0"
|
react-redux "^7.2.0"
|
||||||
react-motion "^0.5.2"
|
redux "^4.0.4"
|
||||||
react-redux "^5.0.6"
|
use-memo-one "^1.1.1"
|
||||||
redux "^3.7.2"
|
|
||||||
redux-thunk "^2.2.0"
|
|
||||||
reselect "^3.0.1"
|
|
||||||
|
|
||||||
react-clientside-effect@^1.2.2:
|
react-clientside-effect@^1.2.2:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
|
@ -6814,7 +6827,7 @@ react-focus-lock@^2.5.0:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||||
|
|
||||||
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
@ -6824,32 +6837,17 @@ react-is@^17.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||||
|
|
||||||
react-lifecycles-compat@^3.0.0:
|
react-redux@^7.2.0:
|
||||||
version "3.0.4"
|
version "7.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
|
||||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
|
||||||
|
|
||||||
react-motion@^0.5.2:
|
|
||||||
version "0.5.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
|
||||||
integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
performance-now "^0.2.0"
|
"@babel/runtime" "^7.12.1"
|
||||||
prop-types "^15.5.8"
|
"@types/react-redux" "^7.1.16"
|
||||||
raf "^3.1.0"
|
hoist-non-react-statics "^3.3.2"
|
||||||
|
loose-envify "^1.4.0"
|
||||||
react-redux@^5.0.6:
|
prop-types "^15.7.2"
|
||||||
version "5.1.2"
|
react-is "^16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57"
|
|
||||||
integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.1.2"
|
|
||||||
hoist-non-react-statics "^3.3.0"
|
|
||||||
invariant "^2.2.4"
|
|
||||||
loose-envify "^1.1.0"
|
|
||||||
prop-types "^15.6.1"
|
|
||||||
react-is "^16.6.0"
|
|
||||||
react-lifecycles-compat "^3.0.0"
|
|
||||||
|
|
||||||
react-shallow-renderer@^16.13.1:
|
react-shallow-renderer@^16.13.1:
|
||||||
version "16.14.1"
|
version "16.14.1"
|
||||||
|
@ -6978,20 +6976,12 @@ redent@^3.0.0:
|
||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
redux-thunk@^2.2.0:
|
redux@^4.0.0, redux@^4.0.4:
|
||||||
version "2.3.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
|
||||||
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
|
||||||
|
|
||||||
redux@^3.7.2:
|
|
||||||
version "3.7.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
|
|
||||||
integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.2.1"
|
"@babel/runtime" "^7.9.2"
|
||||||
lodash-es "^4.2.1"
|
|
||||||
loose-envify "^1.1.0"
|
|
||||||
symbol-observable "^1.0.3"
|
|
||||||
|
|
||||||
regenerate-unicode-properties@^8.2.0:
|
regenerate-unicode-properties@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
|
@ -7005,11 +6995,6 @@ regenerate@^1.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
|
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
|
||||||
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
|
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
|
||||||
|
|
||||||
regenerator-runtime@^0.11.0:
|
|
||||||
version "0.11.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
|
||||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
|
||||||
|
|
||||||
regenerator-runtime@^0.13.4:
|
regenerator-runtime@^0.13.4:
|
||||||
version "0.13.7"
|
version "0.13.7"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||||
|
@ -7167,11 +7152,6 @@ require-main-filename@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
|
||||||
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
|
||||||
|
|
||||||
reselect@^3.0.1:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
|
|
||||||
integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
|
|
||||||
|
|
||||||
resize-observer-polyfill@^1.5.1:
|
resize-observer-polyfill@^1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||||
|
@ -7894,11 +7874,6 @@ svg-tags@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
|
||||||
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
|
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
|
||||||
|
|
||||||
symbol-observable@^1.0.3:
|
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
|
||||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
|
||||||
|
|
||||||
symbol-tree@^3.2.4:
|
symbol-tree@^3.2.4:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||||
|
@ -7961,6 +7936,11 @@ through@^2.3.6:
|
||||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||||
|
|
||||||
|
tiny-invariant@^1.0.6:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
|
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||||
|
|
||||||
tmatch@^2.0.1:
|
tmatch@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
|
resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
|
||||||
|
@ -8276,6 +8256,11 @@ use-callback-ref@^1.2.1:
|
||||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
|
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
|
||||||
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
|
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
|
||||||
|
|
||||||
|
use-memo-one@^1.1.1:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
|
||||||
|
integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
|
||||||
|
|
||||||
use-sidecar@^1.0.1:
|
use-sidecar@^1.0.1:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"
|
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"
|
||||||
|
|
Loading…
Reference in a new issue