feat: bump-matrix-react-sdk-from-2.5.0-to-2.7.1

pull/1/head
c-cal 5 years ago
commit a9fc85b54c
Signed by: watcha
GPG Key ID: 87DD78E7F7A1581D
  1. 4
      .prettierrc.yaml
  2. 6
      res/css/_components.scss
  3. 2
      res/css/structures/_TabbedView.scss
  4. 18
      res/css/structures/_watcha_NextcloudPanel.scss
  5. 21
      res/css/views/context_menus/_TopLeftMenu.scss
  6. 6
      res/css/views/dialogs/_RoomSettingsDialog.scss
  7. 160
      res/css/views/dialogs/_watcha_InviteMemberDialog.scss
  8. 32
      res/css/views/dialogs/_watcha_NextcloudShareDialog.scss
  9. 12
      res/css/views/elements/_watcha_DelayedSpinner.scss
  10. 9
      res/css/views/room_settings/_watcha_NextcloudSettings.scss
  11. 9
      res/img/watcha/watcha_admin.svg
  12. 11
      res/img/watcha/watcha_paper-plane.svg
  13. 48
      res/themes/watcha/css/_watcha.scss
  14. 6
      res/themes/watcha/css/watcha.scss
  15. 2
      src/CallHandler.js
  16. 6
      src/Modal.js
  17. 6
      src/RoomInvite.js
  18. 3
      src/TextForEvent.js
  19. 1
      src/components/structures/MatrixChat.tsx
  20. 4
      src/components/structures/RightPanel.js
  21. 9
      src/components/structures/RoomView.js
  22. 52
      src/components/structures/watcha_NextcloudPanel.js
  23. 3
      src/components/views/avatars/MemberAvatar.js
  24. 2
      src/components/views/context_menus/MessageContextMenu.js
  25. 2
      src/components/views/context_menus/RoomTileContextMenu.js
  26. 85
      src/components/views/context_menus/TopLeftMenu.js
  27. 2
      src/components/views/context_menus/WidgetContextMenu.js
  28. 8
      src/components/views/dialogs/CreateRoomDialog.js
  29. 62
      src/components/views/dialogs/LogoutDialog.js
  30. 15
      src/components/views/dialogs/RoomSettingsDialog.js
  31. 4
      src/components/views/dialogs/UserSettingsDialog.js
  32. 868
      src/components/views/dialogs/watcha_InviteMemberDialog.js
  33. 136
      src/components/views/dialogs/watcha_NextcloudShareDialog.js
  34. 18
      src/components/views/elements/AppTile.js
  35. 16
      src/components/views/elements/ErrorBoundary.js
  36. 2
      src/components/views/elements/SSOButton.js
  37. 12
      src/components/views/elements/watcha_DelayedSpinner.js
  38. 3
      src/components/views/right_panel/RoomHeaderButtons.js
  39. 110
      src/components/views/room_settings/watcha_NextcloudSettings.js
  40. 8
      src/components/views/rooms/AppsDrawer.js
  41. 18
      src/components/views/rooms/MemberInfo.js
  42. 3
      src/components/views/rooms/MemberList.js
  43. 2
      src/components/views/rooms/MemberTile.js
  44. 6
      src/components/views/rooms/MessageComposer.js
  45. 2
      src/components/views/rooms/RoomHeader.js
  46. 14
      src/components/views/rooms/RoomPreviewBar.js
  47. 3
      src/components/views/settings/AvatarSetting.js
  48. 22
      src/components/views/settings/ProfileSettings.js
  49. 2
      src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
  50. 2
      src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
  51. 2
      src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
  52. 21
      src/components/views/settings/tabs/room/watcha_NextcloudSettingsTab.js
  53. 5
      src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
  54. 12
      src/components/views/settings/tabs/user/HelpUserSettingsTab.js
  55. 2
      src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
  56. 3
      src/languageHandler.js
  57. 30
      src/resizer/resizer.js
  58. 14
      src/settings/Settings.js
  59. 3
      src/settings/watchers/ThemeWatcher.ts
  60. 1
      src/theme.js
  61. 1
      src/utils/WidgetUtils.js
  62. 9
      src/utils/watcha_nextcloudUtils.js

@ -0,0 +1,4 @@
# Prettier rules for Watcha code
arrowParens: avoid
printWidth: 120
tabWidth: 4

@ -220,3 +220,9 @@
@import "./views/voip/_CallView.scss";
@import "./views/voip/_IncomingCallbox.scss";
@import "./views/voip/_VideoView.scss";
// watcha+
@import "./structures/_watcha_NextcloudPanel.scss";
@import "./views/dialogs/_watcha_InviteMemberDialog.scss";
@import "./views/dialogs/_watcha_NextcloudShareDialog.scss";
@import "./views/room_settings/_watcha_NextcloudSettings.scss";
// +watcha

@ -32,6 +32,8 @@ limitations under the License.
max-width: 170px;
color: $tab-label-fg-color;
position: fixed;
width: 180px; // watcha+
max-width: 180px; // watcha+
}
.mx_TabbedView_tabLabel {

@ -0,0 +1,18 @@
.watcha_NextcloudPanel {
height: 100%;
border: 0;
}
.watcha_NextcloudPanel_settingsIcon-noWrap {
white-space: nowrap;
&::after {
display: inline-block;
width: 1.2em;
height: 1.2em;
vertical-align: middle;
mask-size: contain;
mask-image: url("$(res)/img/feather-customised/settings.svg");
background-color: $roomheader-button-color;
content: "";
}
}

@ -49,6 +49,12 @@ limitations under the License.
padding: 0;
list-style: none;
// watcha+
.mx_TopLeftMenu_icon_signout {
border-bottom: 1px solid black;
}
// +watcha
.mx_TopLeftMenu_icon_home::after {
mask-image: url('$(res)/img/feather-customised/home.svg');
}
@ -69,6 +75,21 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/sign-out.svg');
}
// watcha+
.mx_TopLeftMenu_icon_admin::after {
mask-image: url('$(res)/img/watcha/watcha_admin.svg');
}
.mx_TopLeftMenu_icon_jitsi::after {
mask-image: url('$(res)/img/feather-customised/video.svg');
}
.mx_TopLeftMenu_icon_nextcloud::after {
mask-image: url('$(res)/img/feather-customised/files.svg');
mask-size: contain !important;
}
// +watcha
.mx_AccessibleButton::after {
mask-repeat: no-repeat;
mask-position: 0 center;

@ -42,6 +42,12 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/warning-triangle.svg');
}
// watcha+
.mx_RoomSettingsDialog_nextcloudIcon::before {
mask-image: url('$(res)/img/feather-customised/files.svg');
}
// +watcha
.mx_RoomSettingsDialog .mx_Dialog_title {
-ms-text-overflow: ellipsis;
text-overflow: ellipsis;

@ -0,0 +1,160 @@
.watcha_InviteMemberDialog {
.mx_Dialog_content {
display: flex;
justify-content: space-between;
height: 27em;
& > * {
width: 48%;
}
}
.mx_SearchBox {
margin: 0 0 0.5em 0;
}
.mx_EntityTile {
margin: 0 1em 0.5em 1em;
}
}
.watcha_InviteMemberDialog_Section {
display: flex;
flex-direction: column;
h2 {
text-align: center;
font-weight: bold;
}
.mx_EntityTile:hover::before {
mask-position: center !important;
}
}
.watcha_InviteMemberDialog_sourceContainer {
display: flex;
flex-direction: column;
}
.watcha_InviteMemberDialog_Section_suggestedList {
min-height: 0;
flex-grow: 1;
}
.watcha_InviteMemberDialog_Section_emailInvitation {
margin-top: 1em;
}
.watcha_InviteMemberDialog_SuggestedList,
.watcha_InviteMemberDialog_SelectedList {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0.5em;
border: 1px solid $input-darker-fg-color;
border-radius: 5px;
p {
margin-top: 2.5em;
text-align: center;
}
}
.watcha_InviteMemberDialog_SuggestedList {
flex-grow: 1;
}
.watcha_InviteMemberDialog_SelectedList {
height: 100%;
transition: border-color 0.25s;
}
.watcha_InviteMemberDialog_SelectedList_invalid {
border-color: $input-invalid-border-color;
}
.watcha_InviteMemberDialog_SelectedList_hint {
color: $input-darker-fg-color;
strong {
font-weight: bold;
}
}
.watcha_InviteMemberDialog_EntityTile_roomMember {
cursor: initial;
&.mx_EntityTile:hover {
padding-right: initial;
&::before {
display: none;
}
}
}
.watcha_InviteMemberDialog_EntityTile_invite {
border: 3px solid transparent;
border-radius: 5px;
animation-name: flash;
animation-duration: 1s;
&.mx_EntityTile:hover::before {
mask: url("$(res)/img/cancel-small.svg");
mask-repeat: no-repeat;
}
}
.watcha_InviteMemberDialog_EntityTile_partner {
.mx_BaseAvatar_image {
border-radius: 0;
}
}
@keyframes flash {
30% {
border-color: aqua;
}
}
.watcha_EmailInvitation > p {
margin-top: 0;
}
.watcha_InviteMemberDialog_emailAddressContainer {
display: flex;
height: 2.5em;
.mx_Field {
margin: 0;
&:not(:focus-within) {
border-color: $input-darker-fg-color;
}
}
}
.watcha_InviteMemberDialog_addEmailAddressButton {
display: flex;
width: 3em;
margin-left: 0.5em;
border: 1px solid $input-darker-fg-color;
border-radius: 4px;
box-sizing: border-box;
> span {
width: 100%;
mask-image: url("$(res)/img/member_chevron.png");
mask-repeat: no-repeat;
mask-position: center;
background-color: $rightpanel-button-color;
}
&:hover {
cursor: pointer;
}
}
.watcha_InviteMemberDialog_addEmailAddressButton_valid {
border: 2px solid aqua;
> span {
animation: slide 1s ease-in-out infinite;
}
}
@keyframes slide {
0%,
100% {
transform: translate(-0.15em);
}
50% {
transform: translate(0.15em);
}
}

@ -0,0 +1,32 @@
.watcha_NextcloudShareDialog {
height: 80vh;
display: flex;
flex-direction: column;
.mx_Dialog_content {
flex-grow: 1;
}
iframe {
width: 100%;
height: 100%;
border: 0;
border-bottom: 1px solid black;
}
.mx_Field {
flex-grow: 0 !important;
}
}
.watcha_NextcloudShareDialog_Field_rootSelection {
input {
color: red !important;
}
}
.watcha_NextcloudShareDialog_Spinner {
img {
z-index: 0;
position: fixed;
top: calc(50vh - 16px);
}
}

@ -0,0 +1,12 @@
.watcha_DelayedSpinner {
img {
animation: 0s linear 0.5s forwards makeVisible;
visibility: hidden;
}
}
@keyframes makeVisible {
to {
visibility: visible;
}
}

@ -0,0 +1,9 @@
.watcha_NextcloudSettings_Buttons {
display: flex;
justify-content: space-between;
}
span + .watcha_NextcloudSettings_Buttons,
.error + .watcha_NextcloudSettings_Buttons {
margin-top: 1em;
}

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 495.31 495.31" xmlns="http://www.w3.org/2000/svg">
<path d="m454.21 33.394h-413.12c-22.666 0-41.096 18.432-41.096 41.089v241.4c0 22.652 18.43 41.085 41.096 41.085h135.6v25.517c-26.117 2.82-46.605 24.748-46.605 51.604v15.302c0 6.912 5.609 12.522 12.522 12.522h210.08c6.912 0 12.522-5.609 12.522-12.522v-15.302c0-26.855-20.486-48.783-46.605-51.604v-25.517h135.6c22.663 0 41.096-18.433 41.096-41.085v-241.4c-1e-3 -22.657-18.434-41.089-41.097-41.089zm-114.04 400.7v2.779h-185.04v-2.779c0-14.961 12.166-27.122 27.123-27.122h130.79c14.958 0 27.123 12.161 27.123 27.122zm121.74-118.21c0 4.247-3.458 7.695-7.705 7.695h-413.12c-4.251 0-7.705-3.448-7.705-7.695v-241.4c0-4.247 3.454-7.699 7.705-7.699h413.12c4.247 0 7.705 3.452 7.705 7.699v241.4z"/>
<path d="m172.52 213.62h-47.298c-4.419 0-7.997 3.578-7.997 7.997v47.299c0 4.426 3.578 8.004 7.997 8.004h47.298c4.426 0 8.004-3.578 8.004-8.004v-47.299c0-4.419-3.578-7.997-8.004-7.997z"/>
<path d="m272.7 213.62h-47.298c-4.419 0-7.997 3.578-7.997 7.997v47.299c0 4.426 3.578 8.004 7.997 8.004h47.298c4.43 0 8.008-3.578 8.008-8.004v-47.299c0-4.419-3.578-7.997-8.008-7.997z"/>
<path d="m172.52 113.44h-47.298c-4.419 0-7.997 3.579-7.997 7.997v47.298c0 4.426 3.578 8.008 7.997 8.008h47.298c4.426 0 8.004-3.582 8.004-8.008v-47.298c0-4.418-3.578-7.997-8.004-7.997z"/>
<path d="m272.7 113.44h-47.298c-4.419 0-7.997 3.579-7.997 7.997v47.298c0 4.426 3.578 8.008 7.997 8.008h47.298c4.43 0 8.008-3.582 8.008-8.008v-47.298c0-4.418-3.578-7.997-8.008-7.997z"/>
<path d="m372.87 213.62h-47.298c-4.419 0-7.997 3.578-7.997 7.997v47.299c0 4.426 3.578 8.004 7.997 8.004h47.298c4.426 0 8.004-3.578 8.004-8.004v-47.299c0-4.419-3.578-7.997-8.004-7.997z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="m506.96 1.314c-3.119-1.78-6.955-1.75-10.045 0.078l-183.25 108.36c-4.754 2.811-6.329 8.943-3.518 13.697 2.81 4.753 8.942 6.328 13.697 3.518l131.48-77.749-244.91 254.11-121.81-37.266 158.96-94c4.754-2.812 6.329-8.944 3.518-13.698-2.81-4.753-8.943-6.33-13.697-3.518l-178.48 105.54c-3.41 2.017-5.309 5.856-4.84 9.791s3.216 7.221 7.004 8.38l145.47 44.504 64.177 116.81c0.067 0.121 0.136 0.223 0.207 0.314 1.071 1.786 2.676 3.245 4.678 4.087 1.253 0.527 2.57 0.784 3.878 0.784 2.563 0 5.086-0.986 6.991-2.849l73.794-72.12 138.81 42.466c0.96 0.293 1.945 0.438 2.925 0.438 2.116 0 4.206-0.672 5.948-1.961 2.549-1.886 4.053-4.869 4.053-8.039v-393c0-3.591-1.926-6.907-5.045-8.686zm-235.69 327.92c-1.158 1.673-1.779 3.659-1.779 5.694v61.171l-43.823-79.765 193.92-201.21-148.32 214.11zm18.221 82.079v-62.867l48.99 14.988-48.99 47.879zm202.51-21.826-196.5-60.116 196.5-283.66v343.78z" stroke-opacity="0"/>
<path d="m265.04 387.99c-2.4184-4.4026-4.4086-8.027-4.4228-8.0544l-0.0258-0.0497h8.8846v8.0544c0 4.6331-8e-3 8.0544-0.0195 8.0544-0.0107 0-1.9981-3.6021-4.4165-8.0047zm-15.355-27.958c-5.9956-10.913-10.901-19.852-10.901-19.865 0-0.013 6.9009-0.0236 15.335-0.0236h15.335v39.731h-8.8686zm-17.449-31.833-6.5139-11.856 1.3107-1.3585c0.72086-0.74719 30.881-32.045 67.023-69.55l65.713-68.192h8.303c4.5667 0 8.303 0.0391 8.303 0.0868s-23.602 34.155-52.449 75.793c-28.847 41.638-52.707 76.131-53.022 76.65-1.1723 1.9305-1.3467 2.7449-1.4352 6.7019l-0.08 3.5797h-30.639zm127.84-151.32c0.1661-0.17902 8.1257-8.4422 17.688-18.363l17.386-18.037h6.8419l-0.38919 0.56599c-0.21405 0.3113-5.9371 8.5745-12.718 18.363l-12.329 17.797h-16.782zm35.301-36.646c0.13622-0.14061 3.5453-3.6773 7.5757-7.8594l7.328-7.6037h1.3016c1.09 0 1.2996 3e-3 1.2892 0.0211-7e-3 0.0116-2.4559 3.5482-5.4422 7.8592l-5.4297 7.838-6.8703 4.5e-4zm14.907-15.468c4e-3 -5e-3 1.2355-1.2836 2.7363-2.8409l2.7287-2.8314 1.0757-2e-3 -0.0376 0.0544c-0.0207 0.0299-0.90648 1.3088-1.9684 2.8421l-1.9307 2.7877-1.3058 1.6e-4c-1.2289 1.6e-4 -1.3053-3.8e-4 -1.2982-9e-3zm5.6213-5.8353c0.0846-0.0876 0.92699-0.96164 1.8719-1.9424 1.9566-2.0308 1.763-1.8304 1.7662-1.8272 1e-3 1e-3 -0.60919 0.88546-1.3568 1.9647l-1.3592 1.9622-1.0759 0.00198z" fill="#0ff" stroke="#0ff" stroke-width=".022097"/>
<path d="m164.42 347.58c-3.906-3.905-10.236-3.905-14.143 0l-93.352 93.352c-3.905 3.905-3.905 10.237 0 14.143 1.954 1.952 4.513 2.928 7.072 2.928s5.118-0.976 7.071-2.929l93.352-93.352c3.905-3.904 3.905-10.236 0-14.142z"/>
<path d="m40.071 471.93c-3.906-3.903-10.236-3.903-14.142 1e-3l-23 23c-3.905 3.905-3.905 10.237 0 14.143 1.953 1.952 4.512 2.928 7.071 2.928s5.118-0.977 7.071-2.929l23-23c3.905-3.905 3.905-10.237 0-14.143z"/>
<path d="m142.65 494.34c-1.859-1.86-4.439-2.93-7.069-2.93-2.641 0-5.21 1.07-7.07 2.93s-2.93 4.43-2.93 7.07c0 2.63 1.069 5.21 2.93 7.07 1.86 1.86 4.44 2.93 7.07 2.93s5.21-1.07 7.069-2.93c1.86-1.86 2.931-4.44 2.931-7.07 0-2.64-1.07-5.21-2.931-7.07z"/>
<path d="m217.05 419.94c-3.903-3.905-10.233-3.905-14.142 0l-49.446 49.445c-3.905 3.905-3.905 10.237 0 14.142 1.953 1.953 4.512 2.929 7.071 2.929s5.118-0.977 7.071-2.929l49.446-49.445c3.905-3.905 3.905-10.237 0-14.142z"/>
<path d="m387.7 416.14c-3.906-3.904-10.236-3.904-14.142 0l-49.58 49.58c-3.905 3.905-3.905 10.237 0 14.143 1.953 1.952 4.512 2.929 7.071 2.929s5.118-0.977 7.071-2.929l49.58-49.58c3.905-3.905 3.905-10.237 0-14.143z"/>
<path d="m283.5 136.31c-1.86-1.86-4.44-2.93-7.07-2.93s-5.21 1.07-7.07 2.93c-1.859 1.86-2.93 4.44-2.93 7.08 0 2.63 1.07 5.2 2.93 7.06 1.86 1.87 4.44 2.93 7.07 2.93s5.21-1.06 7.07-2.93c1.859-1.86 2.93-4.43 2.93-7.06 0-2.64-1.07-5.22-2.93-7.08z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@ -0,0 +1,48 @@
$watcha-accent-color: rgb(0, 180, 180);
$watcha-accent-alpha-color: rgba(0, 180, 180, 0.5);
$watcha-accent-border: aqua;
$watcha-dark-color: hsl(0, 0%, 25%);
$accent-color: $watcha-accent-color;
$roomtile-selected-bg-color: $watcha-accent-color;
$input-valid-border-color: $accent-color;
$button-bg-color: $accent-color;
$roomsublist-chevron-color: $accent-color;
$tab-label-active-bg-color: $accent-color;
$button-primary-bg-color: $accent-color;
$button-link-fg-color: $accent-color;
$togglesw-on-color: $accent-color;
$reaction-row-button-selected-border-color: $accent-color;
.mx_LeftPanel {
background-color: $watcha-dark-color !important;
}
.mx_RoomList * {
color: #ffffff !important;
}
.mx_RoomTile {
border-radius: 4px;
&:focus {
background-color: unset !important;
border: solid 1px $watcha-accent-border;
}
&:hover {
background-color: $watcha-accent-alpha-color;
}
}
.mx_RoomSubList_badge:not(.mx_RoomSubList_badgeHighlight) > div {
background-color: #ffffff !important;
color: black !important;
}
.mx_RoomSubList_addRoom {
background-color: unset !important;
border: solid 1px $watcha-accent-border;
&::before {
background-color: $watcha-accent-border !important;
}
}

@ -0,0 +1,6 @@
@import "../../../../res/css/_font-sizes.scss";
@import "../../light/css/_paths.scss";
@import "../../light/css/_fonts.scss";
@import "../../light/css/_light.scss";
@import "_watcha.scss";
@import "../../../../res/css/_components.scss";

@ -335,10 +335,12 @@ function _onAction(payload) {
description: _t('You cannot place a call with yourself.'),
});
return;
/* watcha!
} else if (members.length === 2) {
console.info("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
placeCall(call);
!watcha */
} else { // > 2
dis.dispatch({
action: "place_conference_call",

@ -226,8 +226,14 @@ class ModalManager {
};
}
/* watcha!
appendDialogAsync(prom, props, className) {
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {});
!watcha */
// watcha+
appendDialogAsync(prom, props, className, options = {}) {
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options);
// +watcha
this._modals.push(modal);
this._reRender();

@ -40,7 +40,10 @@ export function inviteMultipleToRoom(roomId, addrs) {
export function showStartChatInviteDialog() {
// This dialog handles the room creation internally - we don't need to worry about it.
/* watcha!
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
!watcha */
const InviteDialog = sdk.getComponent("dialogs.watcha_InviteMemberDialog"); // watcha+
Modal.createTrackedDialog(
'Start DM', '', InviteDialog, {kind: KIND_DM},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
@ -49,7 +52,10 @@ export function showStartChatInviteDialog() {
export function showRoomInviteDialog(roomId) {
// This dialog handles the room creation internally - we don't need to worry about it.
/* watcha!
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
!watcha */
const InviteDialog = sdk.getComponent("dialogs.watcha_InviteMemberDialog"); // watcha+
Modal.createTrackedDialog(
'Invite Users', '', InviteDialog, {kind: KIND_INVITE, roomId},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,

@ -471,7 +471,10 @@ function textForPinnedEvent(event) {
}
function textForWidgetEvent(event) {
/* watcha!
const senderName = event.getSender();
*watcha */
const senderName = event.sender ? event.sender.name : event.getSender(); // watcha+
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
const {name, type, url} = event.getContent() || {};

@ -637,6 +637,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewGroup(payload);
break;
case 'view_welcome_page':
dis.dispatch({action: 'start_login'}); // watcha+
this.viewWelcome();
break;
case 'view_home_page':

@ -196,6 +196,7 @@ export default class RightPanel extends React.Component {
const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
const FilePanel = sdk.getComponent('structures.FilePanel');
const NextcloudPanel = sdk.getComponent('structures.watcha_NextcloudPanel'); // watcha+
const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
@ -296,7 +297,10 @@ export default class RightPanel extends React.Component {
panel = <NotificationPanel />;
break;
case RIGHT_PANEL_PHASES.FilePanel:
/* watcha!
panel = <FilePanel roomId={this.props.roomId} resizeNotifier={this.props.resizeNotifier} />;
!watcha */
panel = <NextcloudPanel roomId={this.props.roomId} />; // watcha+
break;
}

@ -928,6 +928,15 @@ export default createReactClass({
},
onRoomStateEvents: function(ev, state) {
// watcha+
if (
ev.getType() == "im.vector.web.settings" &&
ev.getContent().hasOwnProperty("nextcloudShare")
) {
this.forceUpdate();
}
// +watcha
// ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) {
return;

@ -0,0 +1,52 @@
import React, { useRef } from "react";
import SettingsStore from "../../settings/SettingsStore";
import { _t } from "../../languageHandler";
import { refineNextcloudIframe } from "../../utils/watcha_nextcloudUtils";
export default ({ roomId }) => {
const nextcloudIframeRef = useRef();
let panel;
if (SettingsStore.isFeatureEnabled("feature_nextcloud")) {
const nextcloudFolder = SettingsStore.getValue("nextcloudShare", roomId);
if (nextcloudFolder) {
panel = (
<iframe
id="watcha_NextcloudPanel"
ref={nextcloudIframeRef}
className="watcha_NextcloudPanel"
src={nextcloudFolder}
onLoad={() => {
refineNextcloudIframe(nextcloudIframeRef);
}}
/>
);
} else {
let hint;
if (SettingsStore.canSetValue("nextcloudShare", roomId, "room")) {
hint = (
<p>
{_t(
"You can choose one from room <span>settings </span>",
{},
{
span: sub => <span className="watcha_NextcloudPanel_settingsIcon-noWrap">{sub}</span>,
}
)}
</p>
);
}
panel = (
<div className={"mx_RoomView_messageListWrapper"}>
<div className="mx_RoomView_empty">
{_t("No folder is shared")}
{hint}
</div>
</div>
);
}
}
return panel;
};

@ -61,7 +61,10 @@ export default createReactClass({
if (props.member && props.member.name) {
return {
name: props.member.name,
/* watcha!
title: props.title || props.member.userId,
!watcha */
title: props.title, // watcha+
imageUrl: props.member.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
Math.floor(props.width * window.devicePixelRatio),

@ -479,7 +479,9 @@ export default createReactClass({
{ cancelButton }
{ forwardButton }
{ pinButton }
{/* watcha!
{ viewSourceButton }
!watcha */}
{ viewClearSourceButton }
{ unhidePreviewButton }
{ permalinkButton }

@ -364,6 +364,7 @@ export default createReactClass({
src={require("../../../../res/img/icon_context_low.svg")}
srcSet={require("../../../../res/img/icon_context_low_on.svg")}
/>
{/* watcha!
<RoomTagOption
active={this.state.isDirectMessage}
label={_t('Direct Chat')}
@ -371,6 +372,7 @@ export default createReactClass({
src={require("../../../../res/img/icon_context_person.svg")}
srcSet={require("../../../../res/img/icon_context_person_on.svg")}
/>
!watcha */}
</div>
);
},

@ -28,6 +28,8 @@ import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
import {Jitsi} from "../../../widgets/Jitsi"; // watcha+
import SettingsStore from "../../../settings/SettingsStore"; // watcha+
export default class TopLeftMenu extends React.Component {
static propTypes = {
@ -46,8 +48,24 @@ export default class TopLeftMenu extends React.Component {
this.openSettings = this.openSettings.bind(this);
this.signIn = this.signIn.bind(this);
this.signOut = this.signOut.bind(this);
this.state = { isSynapseAdministrator: false }; // watcha+
}
// watcha+
componentDidMount() {
MatrixClientPeg.get()
.isSynapseAdministrator()
.then(isSynapseAdministrator => {
this.setState({ isSynapseAdministrator });
})
.catch(error => {
if (error.errcode !== "M_FORBIDDEN") {
console.error(`[watcha] ${error.message} - ${error.errcode}`);
}
});
}
// +watcha
hasHomePage() {
return !!getHomePageUrl(SdkConfig.get());
}
@ -73,13 +91,17 @@ export default class TopLeftMenu extends React.Component {
}
let homePageItem = null;
/* watcha!
if (this.hasHomePage()) {
!watcha */
homePageItem = (
<MenuItem className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
{_t("Home")}
</MenuItem>
);
/* watcha!
}
!watcha */
let signInOutItem;
if (isGuest) {
@ -108,17 +130,62 @@ export default class TopLeftMenu extends React.Component {
</MenuItem>
);
// watcha+
let adminItem;
if (this.state.isSynapseAdministrator) {
adminItem = (
<MenuItem
className="mx_TopLeftMenu_icon_admin"
onClick={this.openAdmin}
title={_t("Open the administration console in a new tab")}
>
{_t("Administration")}
</MenuItem>
);
}
const jitsiItem = (
<MenuItem
className="mx_TopLeftMenu_icon_jitsi"
onClick={this.openJitsi}
title={_t("Open the videoconferencing platform in a new tab")}
>
{_t("Videoconferencing")}
</MenuItem>
);
let nextcloudItem;
if (SettingsStore.isFeatureEnabled("feature_nextcloud")) {
nextcloudItem = (
<MenuItem
className="mx_TopLeftMenu_icon_nextcloud"
onClick={this.openNextcloud}
title={_t("Open my documents in a new tab")}
>
{_t("My documents")}
</MenuItem>
);
}
// +watcha
return <div className="mx_TopLeftMenu" ref={this.props.containerRef} role="menu">
{/* watcha!
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true} tabIndex={-1}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
!watcha */}
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
{homePageItem}
{settingsItem}
{/* watcha!
{helpItem}
!watcha */}
{adminItem} {/* watcha+ */}
{signInOutItem}
{nextcloudItem} {/* watcha+ */}
{jitsiItem} {/* watcha+ */}
</ul>
</div>;
}
@ -152,4 +219,22 @@ export default class TopLeftMenu extends React.Component {
closeMenu() {
if (this.props.onFinished) this.props.onFinished();
}
// watcha+
openAdmin = () => {
this.closeMenu();
window.open("/admin", "admin");
}
openJitsi = () => {
this.closeMenu();
const jitsiDomain = "https://" + Jitsi.getInstance().preferredDomain;
window.open(jitsiDomain);
}
openNextcloud = () => {
this.closeMenu();
window.open("/nextcloud", "nextcloud");
}
// +watcha
}

@ -105,12 +105,14 @@ export default class WidgetContextMenu extends React.Component {
);
}
/* watcha!
// Push this last so it appears last. It's always present.
options.push(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
{_t("Remove for me")}
</MenuItem>,
);
!watcha */
// Put separators between the options
if (options.length > 1) {

@ -74,13 +74,17 @@ export default createReactClass({
},
componentDidMount() {
/* watcha!
this._detailsRef.addEventListener("toggle", this.onDetailsToggled);
!watcha */
// move focus to first field when showing dialog
this._nameFieldRef.focus();
},
componentWillUnmount() {
/* watcha!
this._detailsRef.removeEventListener("toggle", this.onDetailsToggled);
!watcha */
},
_onKeyDown: function(event) {
@ -216,12 +220,16 @@ export default createReactClass({
<Field label={ _t('Topic (optional)') } onChange={this.onTopicChange} value={this.state.topic} className="mx_CreateRoomDialog_topic" />
<LabelledToggleSwitch label={ _t("Make this room public")} onChange={this.onPublicChange} value={this.state.isPublic} />
{ publicPrivateLabel }
{/* watcha!
{ e2eeSection }
!watcha */}
{ aliasField }
{/* watcha!
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
</details>
!watcha */}
</div>
</form>
<DialogButtons primaryButton={_t('Create Room')}

@ -82,7 +82,10 @@ export default class LogoutDialog extends React.Component {
_onFinished(confirmed) {
if (confirmed) {
/* watcha!
dis.dispatch({action: 'logout'});
!watcha */
this._oidc_logout() // watcha+
}
// close dialog
this.props.onFinished();
@ -110,14 +113,73 @@ export default class LogoutDialog extends React.Component {
}
_onLogoutConfirm() {
/* watcha!
dis.dispatch({action: 'logout'});
!watcha */
this._oidc_logout() // watcha+
// close dialog
this.props.onFinished();
}
// watcha+
async _oidc_logout() {
const mxClient = MatrixClientPeg.get();
const ssoLoginUrl = mxClient.getSsoLoginUrl("");
fetch(ssoLoginUrl)
.then(response => {
if (response.ok) {
return Promise.resolve(response.url);
} else {
return Promise.reject(
"this homeserver does not handle SSO"
);
}
})
.then(authUrl => {
const match = authUrl.match(".+/realms/[^/]+");
if (match) {
const realmUrl = match[0];
const oidcConfigUrl =
realmUrl + "/.well-known/openid-configuration";
return fetch(oidcConfigUrl);
} else {
return Promise.reject(
"this homeserver is not configured for OpendID"
);
}
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return Promise.reject("OpendID configuration not found");
}
})
.then(oidcConfig => {
if (oidcConfig.end_session_endpoint) {
const sloUrl = new URL(oidcConfig.end_session_endpoint);
sloUrl.searchParams.set(
"redirect_uri",
window.location.origin
);
window.location.href = sloUrl;
} else {
return Promise.reject("end session endpoint not found");
}
})
.catch(error => {
console.warn("Single Logout failed:", error);
});
dis.dispatch({action: 'logout'});
}
// +watcha
render() {
/* watcha!
if (this.state.shouldLoadBackupStatus) {
!watcha */
if (false) { // watcha+
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const description = <div>

@ -29,6 +29,7 @@ import * as sdk from "../../../index";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore";
import NextcloudSettingsTab from "../settings/tabs/room/watcha_NextcloudSettingsTab"; // watcha+
export default class RoomSettingsDialog extends React.Component {
static propTypes = {
@ -60,6 +61,20 @@ export default class RoomSettingsDialog extends React.Component {
"mx_RoomSettingsDialog_settingsIcon",
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
));
// watcha+
if (
SettingsStore.isFeatureEnabled("feature_nextcloud") &&
SettingsStore.canSetValue("nextcloudShare", this.props.roomId, "room")
) {
tabs.push(
new Tab(
_td("Document sharing"),
"mx_RoomSettingsDialog_nextcloudIcon",
<NextcloudSettingsTab roomId={this.props.roomId} />
)
);
}
// +watcha
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_RoomSettingsDialog_securityIcon",

@ -72,11 +72,13 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />,
));
/* watcha!
tabs.push(new Tab(
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
));
!watcha */
tabs.push(new Tab(
_td("Notifications"),
"mx_UserSettingsDialog_bellIcon",
@ -92,11 +94,13 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />,
));
/* watcha!
tabs.push(new Tab(
_td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
!watcha */
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab(
_td("Labs"),

@ -0,0 +1,868 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
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 classNames from "classnames";
import PropTypes from "prop-types";
import React, { Component } from "react";
import { _t } from "../../../languageHandler";
import { inviteMultipleToRoom } from "../../../RoomInvite";
import { Key } from "../../../Keyboard";
import { KIND_DM } from "../../../components/views/dialogs/InviteDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import * as Avatar from "../../../Avatar";
import * as Email from "../../../email";
import * as sdk from "../../../index";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import BaseAvatar from "../avatars/BaseAvatar";
import createRoom, { canEncryptToAllUsers } from "../../../createRoom";
import dis from "../../../dispatcher/dispatcher";
import DMRoomMap from "../../../utils/DMRoomMap";
import EntityTile from "../rooms/EntityTile";
import SettingsStore from "../../../settings/SettingsStore";
import withValidation from "../elements/Validation";
const AVATAR_SIZE = 36;
class InviteMemberDialog extends Component {
static defaultProps = {
kind: KIND_DM,
};
static propTypes = {
// Takes an array of user IDs/emails to invite.
onFinished: PropTypes.func.isRequired,
// The kind of invite being performed. Assumed to be KIND_DM if not provided.
kind: PropTypes.string,
// The room ID this dialog is for. Only required for KIND_INVITE.
roomId: PropTypes.string,
};
constructor(props) {
super(props);
this.state = {
// List of UserAddressType objects representing the set of auto-completion results for the current search query
suggestedList: [],
// List of people that will be invited
selectedList: [],
// Whether a search is ongoing
busy: false,
// An error message generated during the user directory search
searchError: null,
// Whether the server supports the user_directory API
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
pendingSubmission: false,
errorText: null,
};
}
componentDidMount() {
this._doUserDirectorySearch(this.state.query);
}
onOk = () => this.setState({ pendingSubmission: true });
onSearch = value => this._doUserDirectorySearch(value);
getBaseAvatar = (user, name, url) => {
if (!name) {
name = user.displayName || user.address;
}
if (!url) {
url = Avatar.avatarUrlForUser(user, AVATAR_SIZE, AVATAR_SIZE);
}
return <BaseAvatar width={AVATAR_SIZE} height={AVATAR_SIZE} {...{ name, url }} />;
};
getMembership = (roomMembers, userId) => {
for (const member of roomMembers) {
if (member.userId === userId) {
return member.membership;
}
}
};
getSelectedTiles = () => {
const selectedTiles = this.state.selectedList.map(user => {
const commonProps = {
key: user.address,
className: "watcha_InviteMemberDialog_EntityTile_invite",
name: user.displayName,
title: _t("Click to remove this invitation"),
showPresence: false,
};
return user.isKnown ? (
<EntityTile
{...commonProps}
avatarJsx={this.getBaseAvatar(user)}
onClick={e => this.removeFromSelectedList(user)}
subtextLabel={user.displayName !== user.email ? user.email : undefined}
/>
) : (
<EntityTile
{...commonProps}
className={classNames(commonProps.className, "watcha_InviteMemberDialog_EntityTile_partner")}
subtextLabel={_t("An invitation will be sent to this email address")}
avatarJsx={this.getBaseAvatar(
user,
null,
require("../../../../res/img/watcha/watcha_paper-plane.svg")
)}
onClick={e => this.removeEmailAddressFromSelectedList(user)}
/>
);
});
if (selectedTiles.length > 0) {
return selectedTiles;
}
};
getSuggestedTiles = () => {
const suggestedTiles = this.state.suggestedList.map(user => {
const commonProps = {
key: user.address,
name: user.displayName,
avatarJsx: this.getBaseAvatar(user),
};
const subtextLabel = {
join: _t("Already room member"),
invite: _t("Already invited"),
};
return subtextLabel.hasOwnProperty(user.membership) ? (
<EntityTile
{...commonProps}
className="watcha_InviteMemberDialog_EntityTile_roomMember"
subtextLabel={subtextLabel[user.membership]}
presenceState="offline"
suppressOnHover={true}
/>
) : (
<EntityTile
{...commonProps}
title={_t("Click to add this user to the invitation list")}
showPresence={false}
onClick={e => this.addToSelectedList(user)}
subtextLabel={user.displayName !== user.email ? user.email : undefined}
/>
);
});
if (suggestedTiles.length > 0) {
return suggestedTiles;
}
};
// strongly inspired from src/components/views/dialogs/AddressPickerDialog.js
// getUsers() is really unreliable: not all users appear according to context
// (who does the search, new user, user who has never been invited)
_doLocalSearch = query => {
this.setState({
query,
searchError: null,
});
const queryLowercase = query.toLowerCase();
const results = [];
MatrixClientPeg.get()
.getUsers()
.forEach(user => {
if (
user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
) {
return;
}
// Put results in the format of the new API
results.push({
user_id: user.userId,
display_name: user.displayName,
avatar_url: user.avatarUrl,
});
});
this._processResults(results, query);
};
// strongly inspired from src/components/views/dialogs/AddressPickerDialog.js
_doUserDirectorySearch = query => {
this.setState({
busy: true,
query,
searchError: null,
});
MatrixClientPeg.get()
.searchUserDirectory({
term: query,
limit: Number.MAX_SAFE_INTEGER,
})
.then(resp => {
// The query might have changed since we sent the request, so ignore
// responses for anything other than the latest query.
if (this.state.query !== query) {
return;
}
this._processResults(resp.results, query);
})
.catch(err => {
console.error("Error whilst searching user directory: ", err);
this.setState({
searchError: err.errcode ? err.message : _t("Something went wrong!"),
});
if (err.errcode === "M_UNRECOGNIZED") {
this.setState({ serverSupportsUserDirectory: false });
// Do a local search immediately
this._doLocalSearch(query);
}
})
.then(() => {
this.setState({ busy: false });
});
};
// copied from src/components/views/dialogs/InviteDialog.js
_inviteUsers = () => {
this.setState({ busy: true });
const targetIds = this.state.selectedList.map(user => user.address);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) {
console.error("Failed to find the room to invite users to");
this.setState({
busy: false,
errorText: _t("Something went wrong trying to invite the users."),
});
return;
}
inviteMultipleToRoom(this.props.roomId, targetIds)
.then(result => {
if (!this._shouldAbortAfterInviteError(result)) {
// handles setting error message too
this.props.onFinished();
}
})
.catch(err => {
console.error(err);
this.setState({
busy: false,
errorText: _t(
"We couldn't invite those users. Please check the users you want to invite and try again."
),
});
});
};
_processResults = (results, query) => {
const suggestedList = [];
const client = MatrixClientPeg.get();
for (const user of results) {
const userId = user.user_id;
if (userId === client.credentials.userId) {
continue; // remove the actual user from the list of users
}
const email = user.email;
const displayName = user.display_name || email || userId;
let membership;
if (this.props.roomId) {
const room = client.getRoom(this.props.roomId);
const roomMembers = Object.values(room.currentState.members);
membership = this.getMembership(roomMembers, userId);
}
// watcha TODO: as the upstream interface has changed, it should be simplified by removing useless fields
if (this.state.selectedList.every(user => user.address != userId)) {
suggestedList.push({
address: userId,
addressType: "mx-user-id",
avatarUrl: user.avatar_url,
isKnown: true,
displayName,
membership,
email,
});
}
}
this.setState({ suggestedList: this.sortedUserList(suggestedList) });
};
// copied from src/components/views/dialogs/InviteDialog.js
_shouldAbortAfterInviteError(result) {
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === "error");
if (failedUsers.length > 0) {
console.log("Failed to invite users: ", result);
this.setState({
busy: false,
errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
csvUsers: failedUsers.join(", "),
}),
});
return true; // abort
}
return false;
}
// copied from src/components/views/dialogs/InviteDialog.js
_startDm = async () => {
this.setState({ busy: true });
const targetIds = this.state.selectedList.map(user => user.address);
// Check if there is already a DM with these people and reuse it if possible.
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
if (existingRoom) {
dis.dispatch({
action: "view_room",
room_id: existingRoom.roomId,
should_peek: false,
joining: false,
});
this.props.onFinished();
return;
}
const createRoomOptions = { inlineErrors: true };
if (SettingsStore.getValue("feature_cross_signing")) {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
// watcha TODO: check why is it a problem
const has3PidMembers = this.state.selectedList.some(user => user.addressType === "email");
if (!has3PidMembers) {
const client = MatrixClientPeg.get();
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
}
}
// Check if it's a traditional DM and create the room if required.
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
let createRoomPromise = Promise.resolve();
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
if (targetIds.length === 1 && !isSelf) {
createRoomOptions.dmUserId = targetIds[0];
createRoomPromise = createRoom(createRoomOptions);
} else if (isSelf) {
createRoomPromise = createRoom(createRoomOptions);
} else {
// Create a boring room and try to invite the targets manually.
createRoomPromise = createRoom(createRoomOptions)
.then(roomId => {
return inviteMultipleToRoom(roomId, targetIds);
})
.then(result => {
if (this._shouldAbortAfterInviteError(result)) {
return true; // abort
}
});
}
// the createRoom call will show the room for us, so we don't need to worry about that.
createRoomPromise
.then(abort => {
if (abort === true) return; // only abort on true booleans, not roomIds or something
this.props.onFinished();
})
.catch(err => {
console.error(err);
this.setState({
busy: false,
errorText: _t(
"We couldn't create your DM. Please check the users you want to invite and try again."
),
});
});
};
addEmailAddressToSelectedList = emailAddress => {
let knownUser;
const userId = convertEmailToUserId(emailAddress);
for (const user of this.state.suggestedList) {
if (user.address === userId) {
knownUser = user;
break;
}
}
if (!knownUser) {
const newUser = {
address: emailAddress,
addressType: "email",
displayName: emailAddress,
};
this.setState(({ selectedList }) => ({
selectedList: [newUser, ...selectedList],
}));
} else {
this.addToSelectedList(knownUser);
}
};
addToSelectedList = user => {
this.setState(({ suggestedList, selectedList }) => {
for (let i = 0; i < suggestedList.length; i++) {
if (suggestedList[i] === user) {
suggestedList.splice(i, 1);
return {
suggestedList,
selectedList: [user, ...selectedList],
};
}
}
});
};
removeEmailAddressFromSelectedList = user => {
this.setState(({ selectedList }) => {
for (let i = 0; i < selectedList.length; i++) {
if (selectedList[i] === user) {
selectedList.splice(i, 1);
return { selectedList };
}
}
});
};
removeFromSelectedList = user => {
this.setState(({ suggestedList, selectedList }) => {
for (let i = 0; i < selectedList.length; i++) {
if (selectedList[i] === user) {
suggestedList = this.sortedUserList([...suggestedList, user]);
selectedList.splice(i, 1);
return { suggestedList, selectedList };
}
}
});
};
resume = () => this.setState({ pendingSubmission: false });
sortedUserList = list => {
return list.slice().sort((a, b) => {
const nameA = a.displayName.toLowerCase();
const nameB = b.displayName.toLowerCase();
let comp;
if (nameA < nameB) {
comp = -1;
} else if (nameA > nameB) {
comp = 1;
} else {
comp = 0;
}
return comp;
});
};
render() {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const suggestedTiles = this.getSuggestedTiles();
const selectedTiles = this.getSelectedTiles();
let title;
let roomMembers;
let invite;
if (this.props.kind === KIND_DM) {
title = _t("Start a private conversation");
invite = this._startDm;
} else {
// KIND_INVITE
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
title = _t(
"Invite in the <strong>%(roomName)s</strong> room",
{ roomName: room.name },
{ strong: label => <strong>{label}</strong> }
);
roomMembers = Object.values(room.currentState.members);
invite = this._inviteUsers;
}
return (
<BaseDialog className="watcha_InviteMemberDialog" onFinished={this.props.onFinished} {...{ title }}>
<div className="mx_Dialog_content">
<div className="watcha_InviteMemberDialog_sourceContainer">
<Section
className="watcha_InviteMemberDialog_Section_suggestedList"
header={_t("Invite users")}
>
<SuggestedList busy={this.state.busy} onSearch={this.onSearch} query={this.state.query}>
{suggestedTiles}
</SuggestedList>
</Section>
<Section
className="watcha_InviteMemberDialog_Section_emailInvitation"
header={_t("Invite by email")}
>
<EmailInvitation
{...{ roomMembers }}
selectedList={this.state.selectedList}
addEmailAddressToSelectedList={this.addEmailAddressToSelectedList}
/>
</Section>
</div>
<Section header={_t("Invitation list")}>
<SelectedList
pendingSubmission={this.state.pendingSubmission}
resume={this.resume}
{...{ invite }}
>
{selectedTiles}
</SelectedList>
</Section>
</div>
<div className="error">{this.state.errorText}</div>
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this.onOk}
onCancel={this.props.onFinished}
/>
</BaseDialog>
);
}
}
class EmailInvitation extends Component {
static propTypes = {
addEmailAddressToSelectedList: PropTypes.func.isRequired,
selectedList: PropTypes.arrayOf(PropTypes.object).isRequired,
roomMembers: PropTypes.arrayOf(PropTypes.object),
};
constructor(props) {
super(props);
this.state = {
emailAddress: "",
isValid: false,
emailLooksValid: false,
pendingSubmission: false,
};
}
onChange = event => {
this.setState({ emailAddress: event.target.value });
};
onClick = () => {
this.setState({ pendingSubmission: true }, async () => {
await this.submit();
this.setState({ pendingSubmission: false });
});
};
onKeyDown = event => {
if (event.key === Key.ENTER) {
this.onClick();
event.preventDefault();
event.stopPropagation();
}
};
onValidate = async fieldState => {
const result = await this._validationRules(fieldState);
const emailLooksValid = Email.looksValid(this._fieldRef.input.value);
this.setState({
isValid: result.valid,
emailLooksValid,
});
return result;
};
_validationRules = withValidation({
rules: [
{
key: "notNull",
test: async ({ value }) => !!value,
},
{
key: "isValidOnSubmit",
test: async ({ value }) => !value || !this.state.pendingSubmission || Email.looksValid(value),
invalid: () => _t("Please enter a valid email address"),
},
{
key: "emailAlreadyInInvitations",
test: async ({ value }) => !value || !this.props.selectedList.some(user => user.address === value),
invalid: () => _t("You have already added this email address to the invitation list"),
},
{
key: "userAlreadyInInvitations",
test: async ({ value }) => !value || !this.props.selectedList.some(user => user.email === value),
invalid: () => _t("This email address belongs to a user you have already added to the invitation list"),
},
{
key: "alreadyRoomMember",
test: async ({ value }) =>
!value ||
!this.props.roomMembers ||
!this.props.roomMembers.some(
user => user.userId === convertEmailToUserId(value) && user.membership === "join"
),
invalid: () => _t("This email address belongs to a user who is already a room member"),
},
{
key: "alreadySentInvitation",
test: async ({ value }) =>
!value ||
!this.props.roomMembers ||
!this.props.roomMembers.some(
user => user.userId === convertEmailToUserId(value) && user.membership === "invite"
),
invalid: () => _t("An invitation has already been sent to this email address"),
},
],
});
submit = async () => {
const activeElement = document.activeElement;
if (activeElement) {
activeElement.blur();
}
const field = this._fieldRef;
await field.validate({ allowEmpty: false });
// Validation and state updates are async, so we need to wait for them to complete
// first. Queue a `setState` callback and wait for it to resolve.
await new Promise(resolve => this.setState({}, resolve));
if (this.state.isValid) {
this.props.addEmailAddressToSelectedList(this.state.emailAddress);
this.setState({ emailAddress: "" });
}
field.focus();
if (!this.state.isValid) {
field.validate({ allowEmpty: false, focused: true });
}
};
render() {
const Field = sdk.getComponent("views.elements.Field");
return (
<div className="watcha_EmailInvitation">
<div className="watcha_InviteMemberDialog_emailAddressContainer" onKeyDown={this.onKeyDown}>
<Field
id="emailAddress"
ref={ref => (this._fieldRef = ref)}
label={_t("Email")}
placeholder="joe@example.com"
value={this.state.emailAddress}
onChange={this.onChange}
onValidate={this.onValidate}
className="mx_CreateRoomDialog_name"
/>
<div
className={classNames("watcha_InviteMemberDialog_addEmailAddressButton", {
watcha_InviteMemberDialog_addEmailAddressButton_valid:
this.state.isValid && this.state.emailLooksValid,
})}
title={_t("Add an email address to the invitation list")}
onClick={this.onClick}
>
<span />
</div>
</div>
</div>
);
}
}
function Section({ className, header, children }) {
return (
<div className={classNames("watcha_InviteMemberDialog_Section", className)}>
<h2>{header}</h2>
{children}
</div>
);
}
Section.propTypes = {
header: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
function SuggestedList({ busy, query, onSearch, children }) {
const Spinner = sdk.getComponent("views.elements.Spinner");
const SearchBox = sdk.getComponent("structures.SearchBox");
const hint = busy ? (
<Spinner />
) : (
<p>
{_t(query ? "No users match your search." : "No user can be invited to join this room from the directory.")}
</p>
);
return (
<div className="watcha_InviteMemberDialog_SuggestedList">
<SearchBox placeholder={_t("Filter users")} onSearch={onSearch} />
<AutoHideScrollbar>{children || hint}</AutoHideScrollbar>
</div>
);
}
SuggestedList.defaultProps = {
query: "",
};
SuggestedList.propTypes = {
busy: PropTypes.bool,
query: PropTypes.string,
onSearch: PropTypes.func.isRequired,
children: PropTypes.node,
};
class SelectedList extends Component {
static propTypes = {
pendingSubmission: PropTypes.bool.isRequired,
invite: PropTypes.func.isRequired,
resume: PropTypes.func.isRequired,
children: PropTypes.node,
};
constructor() {
super();
this.state = {
valid: false,
feedback: null,
feedbackVisible: false,
};
}
componentDidUpdate(prevProps) {
if (prevProps.pendingSubmission !== this.props.pendingSubmission && this.props.pendingSubmission) {
this.validate({ focused: true }).then(valid => {
if (valid) {
this.props.invite();
} else {
this.div.focus();
this.props.resume();
}
});
}
}
onBlur = () => {
this.setState({ feedbackVisible: false });
};
_validationRules = withValidation({
rules: [
{
key: "emptyInvitList",
test: async ({ value }) => Array.isArray(value) && value.length > 0,
invalid: () => _t("Please add people to the invitation list before validating the form"),
},
],
});
async validate({ focused, allowEmpty = false }) {
const value = this.props.children;
const { valid, feedback } = await this._validationRules({
value,
focused,
allowEmpty,
});
if (feedback) {
this.setState({ feedback, feedbackVisible: true });
} else {
this.setState({ feedbackVisible: false });
}
return valid;
}
render() {
const hint = (
<div className="watcha_InviteMemberDialog_SelectedList_hint">
<p>
{_t(
"Select the person you want to invite from the <strong>Invite users</strong> list.",
{},
{ strong: label => <strong>{label}</strong> }
)}
</p>
<p>
{_t(
"If the person to invite is not in the list, enter their email address in the <strong>Invite by email</strong> field.",
{},
{ strong: label => <strong>{label}</strong> }
)}
</p>
<p>
{_t("When you validate this form, an email will be sent to them, so that they can join the room.")}
</p>
</div>
);
const feedbackVisible = this.state.feedbackVisible;
const divClasses = classNames("watcha_InviteMemberDialog_SelectedList", {
watcha_InviteMemberDialog_SelectedList_invalid: feedbackVisible,
});
const Tooltip = sdk.getComponent("elements.Tooltip");
const tooltip = (
<Tooltip
tooltipClassName={"mx_Field_tooltip"}
visible={this.state.feedbackVisible}
label={this.state.feedback}
/>
);
return (
<div ref={ref => (this.div = ref)} className={divClasses} onBlur={this.onBlur} tabIndex="-1">
<AutoHideScrollbar>{this.props.children || hint}</AutoHideScrollbar>
{tooltip}
</div>
);
}
}
function convertEmailToUserId(email) {
// follows the spec defined at https://github.com/watcha-fr/devops/blob/master/doc_email_userId.md
// on the server watcha.bar.com (as per mx_hs_url var) :
// - converts foo@bar.com to @foo:watcha.bar.com (email of somebody on the company)
// - converts foo@gmail.com to @foo/gmail.com:watcha.bar.com (email of an external partner)
const parts = email.split("@");
let userId = "@" + parts[0];
let homeServerDomain = MatrixClientPeg.get().getDomain();
if (parts.length > 1) {
const host = parts[1];
// remove http:// or https:// at the beginning of the server name
homeServerDomain = homeServerDomain.replace(/^https?:[/]{2}/, "");
// remove port number and the trailing slash if any at the end of the server name (mostly useful for dev environments)
homeServerDomain = homeServerDomain.replace(/(:[\d]+)?[/]?$/, "");
// determine if the email NOT belongs to somebody of the company
if (!RegExp(`${host}$`).test(homeServerDomain)) {
userId += "/" + host;
}
}
userId += ":" + homeServerDomain;
return userId;
}
export default InviteMemberDialog;

@ -0,0 +1,136 @@
import classNames from "classnames";
import PropTypes from "prop-types";
import React, { useEffect, useRef, useState } from "react";
import { _t } from "../../../languageHandler";
import * as sdk from "../../../index";
import Field from "../elements/Field";
import SettingsStore from "../../../settings/SettingsStore";
import { refineNextcloudIframe } from "../../../utils/watcha_nextcloudUtils";
import Spinner from "../elements/watcha_DelayedSpinner";
const NextcloudShareDialog = ({ roomId, targetFolder, setShareDialogIsBusy, onFinished }) => {
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const [nextcloudFolder, setNextcloudFolder] = useState(targetFolder);
const [prevNextcloudFolder, setPrevNextcloudFolder] = useState(null);
const [isBusy, setIsBusy] = useState(false);
const [errorText, setErrorText] = useState(null);
const [isCancel, _setIsCancel] = useState(false);
const isCancelRef = useRef(false);
const nextcloudIframeRef = useRef();
useEffect(() => {
nextcloudIframeRef.current.contentWindow.addEventListener("click", onClick);
}, []);
const onClick = () => {
setErrorText(null);
setNextcloudFolder(nextcloudIframeRef.current.contentWindow.location.href);
};
const onOK = () => {
setShareDialogIsBusy(true);
setIsBusy(true);
setErrorText(null);
setPrevNextcloudFolder(SettingsStore.getValue("nextcloudShare", roomId));
SettingsStore.setValue("nextcloudShare", roomId, "room", nextcloudFolder)
.then(() => {
if (!isCancelRef.current) {
onFinished(nextcloudFolder);
setShareDialogIsBusy(false);
}
})
.catch(error => {
console.error(error);
if (!isCancelRef.current) {
setErrorText(error.message);
setIsBusy(false);
setShareDialogIsBusy(false);
}
});
};
const onCancel = () => {
if (isBusy) {
setIsCancel(true);
setErrorText(_t("Cancelling…"));
SettingsStore.setValue("nextcloudShare", roomId, "room", prevNextcloudFolder)
.then(() => {
onFinished(prevNextcloudFolder);
})
.catch(error => {
console.error(error);
setErrorText(error.message);
setIsBusy(false);
})
.finally(() => {
setShareDialogIsBusy(false);
});
} else {
onFinished();
}
};
const setIsCancel = value => {
_setIsCancel(value);
isCancelRef.current = value;
};
const params = new URL(nextcloudFolder).searchParams;
const path = params.get("dir");
const relativePath = path ? path.replace(/^\//, "") : null;
return (
<React.Fragment>
<BaseDialog
className="watcha_NextcloudShareDialog"
title={_t("Select a folder to share")}
hasCancel={false}
{...{ onFinished }}
>
<div className="mx_Dialog_content">
<iframe
ref={nextcloudIframeRef}
src={targetFolder}
onLoad={() => {
refineNextcloudIframe(nextcloudIframeRef);
refineNextcloudIframe(nextcloudIframeRef, "/app/watcha_nextcloud/shareDiablog.css");
}}
/>
<Field
className={classNames({
watcha_NextcloudShareDialog_Field_rootSelection: !relativePath,
})}
element="input"
label={_t("Selected folder")}
value={relativePath || _t("No folder selected")}
disabled
/>
<div className="error">{errorText}</div>
</div>
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={onOK}
primaryDisabled={isBusy || !relativePath}
disabled={isCancel}
{...{ onCancel }}
/>
</BaseDialog>
{isBusy && <Spinner className="watcha_NextcloudShareDialog_Spinner" />}
</React.Fragment>
);
};
NextcloudShareDialog.propTypes = {
roomId: PropTypes.string.isRequired,
targetFolder: PropTypes.string.isRequired,
setShareDialogIsBusy: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
};
export default NextcloudShareDialog;

@ -585,12 +585,15 @@ export default class AppTile extends React.Component {
// TODO: Namespace themes through some standard
'theme': SettingsStore.getValue("theme"),
'matrix_room_name': this.props.room.name, // watcha+
});
if (vars.conferenceId === undefined) {
// we'll need to parse the conference ID out of the URL for v1 Jitsi widgets
const parsedUrl = new URL(this.props.app.url);
vars.conferenceId = parsedUrl.searchParams.get("confId");
vars.domain = parsedUrl.searchParams.get("domain"); // watcha+
vars.isAudioOnly = parsedUrl.searchParams.get("isAudioConf"); // watcha+
}
return uriFromTemplate(u, vars);
@ -669,6 +672,13 @@ export default class AppTile extends React.Component {
}
_onPopoutWidgetClick() {
// watcha+
dis.dispatch({
action: 'appsDrawer',
show: false,
});
this._endWidgetActions();
// +watcha
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
@ -717,6 +727,11 @@ export default class AppTile extends React.Component {
</div>
);
if (!this.state.hasPermissionToLoad) {
// watcha+
if (this.props.app.id.match(/^(m.)?jitsi_/)) {
this._grantWidgetPermission()
} else {
// +watcha
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
@ -729,6 +744,7 @@ export default class AppTile extends React.Component {
/>
</div>
);
} // watcha+
} else if (this.state.initialising) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
@ -799,6 +815,7 @@ export default class AppTile extends React.Component {
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
if (showEditButton || showDeleteButton || showPictureSnapshotButton || this.props.showReload) { // watcha+
contextMenu = (
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
<WidgetContextMenu
@ -811,6 +828,7 @@ export default class AppTile extends React.Component {
/>
</ContextMenu>
);
} // watcha+
}
return <React.Fragment>

@ -76,6 +76,7 @@ export default class ErrorBoundary extends React.PureComponent {
return <div className="mx_ErrorBoundary">
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
{/* watcha!
<p>{_t(
"Please <newIssueLink>create a new issue</newIssueLink> " +
"on GitHub so that we can investigate this bug.", {}, {
@ -97,6 +98,21 @@ export default class ErrorBoundary extends React.PureComponent {
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
!watcha */}
{/* watcha+ */}
<p>
{_t("Refresh the page to restart the application.")}
</p>
<p>
{_t(
"Should the failure happen again, please contact us at"
)}{" "}
<a href="mailto:contact@watcha.fr">
contact@watcha.fr
</a>
.
</p>
{/* +watcha */}
</div>
</div>;
}

@ -20,11 +20,13 @@ import PropTypes from 'prop-types';
import PlatformPeg from "../../../PlatformPeg";
import AccessibleButton from "./AccessibleButton";
import {_t} from "../../../languageHandler";
import SdkConfig from '../../../SdkConfig'; // watcha+
const SSOButton = ({matrixClient, loginType, fragmentAfterLogin, ...props}) => {
const onClick = () => {
PlatformPeg.get().startSingleSignOn(matrixClient, loginType, fragmentAfterLogin);
};
if (SdkConfig.get()["watcha_sso_auto_redirect"]) onClick(); // watcha+
return (
<AccessibleButton {...props} kind="primary" onClick={onClick}>

@ -0,0 +1,12 @@
import React from "react";
import * as sdk from "../../../index";
import classNames from "classnames";
export default ({ className }) => {
const Spinner = sdk.getComponent("elements.Spinner");
return (
<div className={classNames("watcha_DelayedSpinner", className)}>
<Spinner />
</div>
);
};

@ -25,6 +25,7 @@ import HeaderButtons, {HEADER_KIND_ROOM} from './HeaderButtons';
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
import SettingsStore from '../../../settings/SettingsStore'; // watcha+
const MEMBER_PHASES = [
RIGHT_PANEL_PHASES.RoomMemberList,
@ -80,6 +81,7 @@ export default class RoomHeaderButtons extends HeaderButtons {
}
renderButtons() {
const showFileButton = SettingsStore.isFeatureEnabled("feature_nextcloud") || null; // watcha+
return [
<HeaderButton key="membersButton" name="membersButton"
title={_t('Members')}
@ -87,6 +89,7 @@ export default class RoomHeaderButtons extends HeaderButtons {
onClick={this._onMembersClicked}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
showFileButton && // watcha+
<HeaderButton key="filesButton" name="filesButton"
title={_t('Files')}
isHighlighted={this.isPhase(RIGHT_PANEL_PHASES.FilePanel)}

@ -0,0 +1,110 @@
import classNames from "classnames";
import PropTypes from "prop-types";
import React, { useRef, useState } from "react";
import { _t } from "../../../languageHandler";
import * as sdk from "../../../index";
import AccessibleButton from "../elements/AccessibleButton";
import Field from "../elements/Field";
import Modal from "../../../Modal";
import SettingsStore from "../../../settings/SettingsStore";
import Spinner from "../elements/watcha_DelayedSpinner";
const NextcloudSettings = ({ roomId }) => {
const [nextcloudFolder, setNextcloudFolder] = useState(SettingsStore.getValue("nextcloudShare", roomId));
const [isBusy, setIsBusy] = useState(false);
const [errorText, setErrorText] = useState(null);
const shareDialogIsBusyRef = useRef(false);
const onShare = () => {
setErrorText(null);
const NextcloudShareDialog = sdk.getComponent("dialogs.watcha_NextcloudShareDialog");
const targetFolder = nextcloudFolder || new URL("nextcloud/apps/files/?dir=/", window.location.origin).href;
const setShareDialogIsBusy = value => {
shareDialogIsBusyRef.current = value;
};
const options = {
onBeforeClose: reason => {
return reason == "backgroundClick" && shareDialogIsBusyRef.current ? false : true;
},
};
const modal = Modal.appendTrackedDialog(
"Nextcloud share",
"",
NextcloudShareDialog,
{ roomId, targetFolder, setShareDialogIsBusy },
/*className=*/ null,
options
);
modal.finished.then(([selectedFolder]) => {
if (selectedFolder) {
setNextcloudFolder(selectedFolder);
}
});
};
const onUnshare = () => {
setIsBusy(true);
setErrorText(null);
SettingsStore.setValue("nextcloudShare", roomId, "room", null)
.then(() => {
setNextcloudFolder(null);
})
.catch(error => {
console.error(error);
setErrorText(error.message);
})
.finally(() => {
setIsBusy(false);
});
};
const notice = SettingsStore.getDisplayName("nextcloudShare");
let sharedFolderField;
let stopSharingButton;
if (nextcloudFolder) {
const params = new URL(nextcloudFolder).searchParams;
const path = params.get("dir");
const relativePath = path ? path.replace(/^\//, "") : null;
sharedFolderField = (
<Field
className={classNames({
watcha_NextcloudShareDialog_Field_rootSelection: !relativePath,
})}
element="input"
label={_t("Shared folder")}
value={relativePath}
disabled
/>
);
stopSharingButton = (
<AccessibleButton kind="danger_outline" onClick={onUnshare} disabled={isBusy}>
{_t("Stop sharing")}
</AccessibleButton>
);
}
return (
<React.Fragment>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">{notice}</div>
{sharedFolderField}
<div className="error">{errorText}</div>
<div className="watcha_NextcloudSettings_Buttons">
{stopSharingButton}
{isBusy && <Spinner />}
<AccessibleButton kind="primary" onClick={onShare} disabled={isBusy}>
{_t(nextcloudFolder ? "Change the shared folder" : "Share a folder")}
</AccessibleButton>
</div>
</React.Fragment>
);
};
NextcloudSettings.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default NextcloudSettings;

@ -157,6 +157,12 @@ export default createReactClass({
render: function() {
const apps = this.state.apps.map((app, index, arr) => {
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
// watcha+
// workaround for relative url of v1 widgets
if (app.url.startsWith("/")) {
app.url = new URL(app.url, window.location.origin).toString();
}
// +watcha
return (<AppTile
key={app.id}
@ -208,7 +214,9 @@ export default createReactClass({
{ apps }
{ spinner }
</div>
{/* watcha!
{ this._canUserModify() && addWidget }
!watcha */}
</div>
);
},

@ -73,6 +73,7 @@ export default createReactClass({
devicesLoading: true,
devices: null,
isIgnoring: false,
email: undefined // watcha+
};
},
@ -102,6 +103,14 @@ export default createReactClass({
this._checkIgnoreState();
this._updateStateForNewMember(this.props.member);
// watcha+
cli.getProfileInfo(this.props.member.userId).then(({ email }) => {
if (email) {
this.setState({ email });
}
});
// +watcha
},
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@ -1129,6 +1138,8 @@ export default createReactClass({
/>);
}
const isMe = this.props.member.userId === this.context.getUserId(); // watcha+
return (
<div className="mx_MemberInfo" role="tabpanel">
<div className="mx_MemberInfo_name">
@ -1141,20 +1152,27 @@ export default createReactClass({
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{/* watcha!
{ this.props.member.userId }
!watcha */}
{this.state.email || this.props.member.userId} {/* watcha+ */}
</div>
{ roomMemberDetails }
</div>
</div>
<AutoHideScrollbar className="mx_MemberInfo_scrollContainer">
<div className="mx_MemberInfo_container">
{ !isMe && <React.Fragment> {/* watcha+ */}
{ this._renderUserOptions() }
{ adminTools }
</React.Fragment> } {/* watcha+ */}
{ startChat }
{/* watcha!
{ this._renderDevices() }
!watcha */}
{ spinner }
</div>

@ -466,7 +466,10 @@ export default createReactClass({
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
inviteButton =
/* watcha!
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
!watcha */
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite} title={ canInvite ? undefined : _t("You do not have permission to invite users to this room")}> {/* watcha+ */}
<span>{ _t('Invite to this room') }</span>
</AccessibleButton>;
}

@ -192,10 +192,12 @@ export default createReactClass({
},
_getDisplayName: function() {
return this.props.member.rawDisplayName; // watcha+
return this.props.member.name;
},
getPowerLabel: function() {
return undefined // watcha+
return _t("%(userName)s (power %(powerLevelNumber)s)", {
userName: this.props.member.userId,
powerLevelNumber: this.props.member.powerLevel,

@ -41,6 +41,7 @@ ComposerAvatar.propTypes = {
};
function CallButton(props) {
return null; // watcha+
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const onVoiceCallClick = (ev) => {
dis.dispatch({
@ -308,7 +309,10 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
/* watcha!
if (SettingsStore.getValue("feature_cross_signing")) {
watcha! */
if (true) { // watcha+
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
@ -371,7 +375,9 @@ export default class MessageComposer extends React.Component {
permalinkCreator={this.props.permalinkCreator} />,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
/* watcha!
<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />,
!watcha */
);
if (this.state.showCallButtons) {

@ -308,7 +308,9 @@ export default createReactClass({
{ settingsButton }
{ pinnedEventsButton }
{ shareRoomButton }
{/* watcha!
{ manageIntegsButton }
!watcha */}
{ forgetButton }
{ searchButton }
</div>;

@ -94,11 +94,22 @@ export default createReactClass({
getInitialState: function() {
return {
busy: false,
email: undefined, // watcha+
};
},
componentDidMount: function() {
this._checkInvitedEmail();
// watcha+
const inviteMember = this._getInviteMember();
if (inviteMember) {
MatrixClientPeg.get().getProfileInfo(inviteMember.userId).then(({ email }) => {
if (email) {
this.setState({ email });
}
});
}
// +watcha
},
componentDidUpdate: function(prevProps, prevState) {
@ -444,7 +455,10 @@ export default createReactClass({
inviterElement = <span>
<span className="mx_RoomPreviewBar_inviter">
{inviteMember.rawDisplayName}
{/* watcha!
</span> ({inviteMember.userId})
!watcha */}
</span> ({this.state.email || inviteMember.userId}) {/* watcha+ */}
</span>;
} else {
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>);

@ -49,7 +49,10 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
<div>&nbsp;</div>
{/* watcha!
{_t("Upload")}
!watcha */}
{_t("Browse")} {/* watcha+ */}
</AccessibleButton>;
}

@ -46,11 +46,30 @@ export default class ProfileSettings extends React.Component {
avatarUrl: avatarUrl,
avatarFile: null,
enableProfileSave: false,
// watcha+
email: undefined,
// workaround https://github.com/vector-im/element-web/issues/13093
// until https://github.com/matrix-org/matrix-react-sdk/pull/5277
displayName: user.displayName !== user.userId ? user.displayName : "",
// +watcha
};
this._avatarUpload = createRef();
}
// watcha+
componentDidMount() {
MatrixClientPeg.get()
.getThreePids()
.then(({ threepids }) => {
const email = threepids.filter(
threepid => threepid.medium === "email"
)[0].address;
this.setState({ email });
});
}
// +watcha
_uploadAvatar = () => {
this._avatarUpload.current.click();
};
@ -150,7 +169,10 @@ export default class ProfileSettings extends React.Component {
<div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls">
<p>
{/* watcha!
{this.state.userId}
!watcha */}
{this.state.email || this.state.userId} {/* watcha+ */}
{hostingSignup}
</p>
<Field label={_t("Display Name")}

@ -150,12 +150,14 @@ export default class AdvancedRoomSettingsTab extends React.Component {
{oldRoomLink}
{roomUpgradeButton}
</div>
{/* watcha!
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<span className='mx_SettingsTab_subheading'>{_t("Developer options")}</span>
<AccessibleButton onClick={this._openDevtools} kind='primary'>
{_t("Open Devtools")}
</AccessibleButton>
</div>
!watcha */}
</div>
);
}

@ -68,6 +68,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
<RoomProfileSettings roomId={this.props.roomId} />
</div>
{/* watcha!
<div className="mx_SettingsTab_heading">{_t("Room Addresses")}</div>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<AliasSettings roomId={this.props.roomId}
@ -88,6 +89,7 @@ export default class GeneralRoomSettingsTab extends React.Component {
</div>
<span className='mx_SettingsTab_subheading'>{_t("Leave room")}</span>
!watcha */}
<div className='mx_SettingsTab_section'>
<AccessibleButton kind='danger' onClick={this._onLeaveClick}>
{ _t('Leave room') }

@ -346,6 +346,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
<div className="mx_SettingsTab mx_SecurityRoomSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
{/* watcha!
<span className='mx_SettingsTab_subheading'>{_t("Encryption")}</span>
<div className='mx_SettingsTab_section mx_SecurityRoomSettingsTab_encryptionSection'>
<div>
@ -357,6 +358,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
</div>
{encryptionSettings}
</div>
!watcha */}
<span className='mx_SettingsTab_subheading'>{_t("Who can access this room?")}</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>

@ -0,0 +1,21 @@
import PropTypes from "prop-types";
import React from "react";
import { _t } from "../../../../../languageHandler";
import NextcloudSettings from "../../../room_settings/watcha_NextcloudSettings";
const NextcloudSettingsTab = ({ roomId }) => (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Document sharing")}</div>
<div className="mx_SettingsTab_section">
<NextcloudSettings {...{ roomId }} />
</div>
</div>
);
NextcloudSettingsTab.propTypes = {
roomId: PropTypes.string.isRequired,
};
export default NextcloudSettingsTab;

@ -285,7 +285,9 @@ export default class GeneralUserSettingsTab extends React.Component {
{passwordChangeText}
</p>
{passwordChangeForm}
{/* watcha!
{threepidSection}
!watcha */}
</div>
);
}
@ -389,11 +391,14 @@ export default class GeneralUserSettingsTab extends React.Component {
{this._renderProfileSection()}
{this._renderAccountSection()}
{this._renderLanguageSection()}
{/* watcha!
<div className="mx_SettingsTab_heading">{discoWarning} {_t("Discovery")}</div>
{this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */}
{/* watcha!
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
!watcha */}
</div>
);
}

@ -118,6 +118,7 @@ export default class HelpUserSettingsTab extends React.Component {
<div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
<ul>
{/* watcha
<li>
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noreferrer noopener" target="_blank">
default cover photo</a> is ©&nbsp;
@ -126,6 +127,7 @@ export default class HelpUserSettingsTab extends React.Component {
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
CC-BY-SA 4.0</a>.
</li>
!watcha */}
<li>
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
target="_blank"> twemoji-colr</a> font is ©&nbsp;
@ -185,8 +187,11 @@ export default class HelpUserSettingsTab extends React.Component {
<div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
<div className="mx_SettingsTab_section">
{/* watcha!
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
!watcha */}
<div className='mx_SettingsTab_subsectionText'>
{/* watcha!
{
_t( "If you've submitted a bug via GitHub, debug logs can help " +
"us track down the problem. Debug logs contain application " +
@ -200,11 +205,13 @@ export default class HelpUserSettingsTab extends React.Component {
{_t("Submit debug logs")}
</AccessibleButton>
</div>
!watcha */}
<div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
</div>
{/* watcha!
{
_t( "To report a Matrix-related security issue, please read the Matrix.org " +
"<a>Security Disclosure Policy</a>.", {},
@ -214,13 +221,16 @@ export default class HelpUserSettingsTab extends React.Component {
rel="noreferrer noopener" target="_blank">{sub}</a>,
})
}
!watcha */}
</div>
</div>
<div className='mx_SettingsTab_section'>
{/* watcha!
<span className='mx_SettingsTab_subheading'>{_t("FAQ")}</span>
<div className='mx_SettingsTab_subsectionText'>
{faqText}
</div>
!watcha */}
<AccessibleButton kind="primary" onClick={KeyboardShortcuts.toggleDialog}>
{ _t("Keyboard Shortcuts") }
</AccessibleButton>
@ -230,7 +240,9 @@ export default class HelpUserSettingsTab extends React.Component {
<div className='mx_SettingsTab_subsectionText'>
{_t("riot-web version:")} {vectorVersion}<br />
{_t("olm version:")} {olmVersion}<br />
{/* watcha!
{updateButton}
!watcha */}
</div>
</div>
{this._renderLegal()}

@ -205,6 +205,7 @@ export default class VoiceUserSettingsTab extends React.Component {
{microphoneDropdown}
{webcamDropdown}
<SettingsFlag name='VideoView.flipVideoHorizontally' level={SettingLevel.ACCOUNT} />
{/* watcha!
<SettingsFlag
name='webRtcAllowPeerToPeer'
level={SettingLevel.DEVICE}
@ -215,6 +216,7 @@ export default class VoiceUserSettingsTab extends React.Component {
level={SettingLevel.DEVICE}
onChange={this._changeFallbackICEServerAllowed}
/>
!watcha */}
</div>
</div>
);

@ -86,7 +86,10 @@ function safeCounterpartTranslate(text, options) {
// in the preferred language, so do it here
translated = counterpart.translate(text, Object.assign({}, options, {locale: 'en'}));
}
/* watcha!
return translated;
!watcha */
return translated.replace(/riot(-web)?/gi, "Watcha"); // watcha+
}
/*

@ -114,19 +114,49 @@ export default class Resizer {
distributor.resizeFromContainerOffset(offset);
};
// watcha+
const nextcloudIframe = document.getElementById("watcha_NextcloudPanel");
const onIframeMouseMove = (event) => {
const offset = sizer.offsetFromEvent(event);
const offsetRelativeToIframe = offset - (window.innerWidth - nextcloudIframe.contentWindow.innerWidth);
distributor.resizeFromContainerOffset(offsetRelativeToIframe);
};
// +watcha
const body = document.body;
const finishResize = () => {
if (this.classNames.resizing) {
this.container.classList.remove(this.classNames.resizing);
}
distributor.finish();
/* watcha!
body.removeEventListener("mouseup", finishResize, false);
document.removeEventListener("mouseleave", finishResize, false);
body.removeEventListener("mousemove", onMouseMove, false);
!watcha */
// watcha+
document.removeEventListener("mouseup", finishResize, false);
document.removeEventListener("mouseleave", finishResize, false);
document.removeEventListener("mousemove", onMouseMove, false);
nextcloudIframe.contentDocument.body.removeEventListener("mouseup", finishResize, false);
nextcloudIframe.contentDocument.body.removeEventListener("mousemove", onIframeMouseMove, false);
// +watcha
};
/* watcha!
body.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false);
body.addEventListener("mousemove", onMouseMove, false);
!watcha */
// watcha+
document.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false);
document.addEventListener("mousemove", onMouseMove, false);
nextcloudIframe.contentDocument.body.addEventListener("mouseup", finishResize, false);
nextcloudIframe.contentDocument.body.addEventListener("mousemove", onIframeMouseMove, false);
// +watcha
}
_createSizerAndDistributor(resizeHandle) {

@ -556,4 +556,18 @@ export const SETTINGS = {
displayName: _td("IRC display name width"),
default: 80,
},
// watcha+
"feature_nextcloud": {
isFeature: true,
supportedLevels: LEVELS_FEATURE,
displayName: _td("Enable Nextcloud integration"),
default: false,
controller: new ReloadOnChangeController(),
},
"nextcloudShare": {
supportedLevels: ["room"],
displayName: _td("Share a Nextcloud folder along its content with room members and use it as a common storage space"),
default: null,
},
// +watcha
};

@ -99,7 +99,10 @@ export default class ThemeWatcher {
// controller that honours the same flag, although probablt better would be to
// have the theme logic in one place rather than split between however many
// different places.
/* watcha!
if (ThemeController.isLogin) return 'light';
!watcha */
if (ThemeController.isLogin) return 'watcha'; // watcha+
// If the user has specifically enabled the system matching option (excluding default),
// then use that over anything else. We pick the lowest possible level for the setting

@ -24,6 +24,7 @@ import ThemeWatcher from "./settings/watchers/ThemeWatcher";
export function enumerateThemes() {
const BUILTIN_THEMES = {
"watcha": "Watcha", // watcha+
"light": _t("Light theme"),
"dark": _t("Dark theme"),
};

@ -455,6 +455,7 @@ export default class WidgetUtils {
'displayName=$matrix_display_name',
'avatarUrl=$matrix_avatar_url',
'userId=$matrix_user_id',
'roomName=$matrix_room_name', // watcha+
].join('&');
let baseUrl = window.location;

@ -0,0 +1,9 @@
export function refineNextcloudIframe(iframeRef, cssLinkHref = "/app/watcha_nextcloud/base.css") {
const cssLink = document.createElement("link");
cssLink.href = cssLinkHref;
cssLink.rel = "stylesheet";
cssLink.type = "text/css";
const iframeDoc = iframeRef.current.contentDocument;
iframeDoc.head.appendChild(cssLink);
iframeDoc.getElementById("header").style.display = "none";
}
Loading…
Cancel
Save