Merge branch 'develop' into fix-notif-jumpy-ui
						commit
						3b6790fbf1
					
				|  | @ -1,3 +1,10 @@ | |||
| Changes in [3.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.1) (2021-02-04) | ||||
| ===================================================================================================== | ||||
| [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0...v3.13.1) | ||||
| 
 | ||||
|  * [Release] Fix z-index of stickerpicker | ||||
|    [\#5618](https://github.com/matrix-org/matrix-react-sdk/pull/5618) | ||||
| 
 | ||||
| Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03) | ||||
| ===================================================================================================== | ||||
| [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0) | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "matrix-react-sdk", | ||||
|   "version": "3.13.0", | ||||
|   "version": "3.13.1", | ||||
|   "description": "SDK for matrix.org using React", | ||||
|   "author": "matrix.org", | ||||
|   "repository": { | ||||
|  |  | |||
|  | @ -21,6 +21,11 @@ limitations under the License. | |||
| 
 | ||||
| $hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic | ||||
| 
 | ||||
| $EventTile_e2e_state_indicator_width: 4px; | ||||
| 
 | ||||
| $MessageTimestamp_width: 46px; /* 8 + 30 (avatar) + 8 */ | ||||
| $MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e_state_indicator_width); | ||||
| 
 | ||||
| :root { | ||||
|     font-size: 10px; | ||||
| } | ||||
|  |  | |||
|  | @ -106,6 +106,7 @@ | |||
| @import "./views/elements/_AddressTile.scss"; | ||||
| @import "./views/elements/_DesktopBuildsNotice.scss"; | ||||
| @import "./views/elements/_DirectorySearchBox.scss"; | ||||
| @import "./views/elements/_DesktopCapturerSourcePicker.scss"; | ||||
| @import "./views/elements/_Dropdown.scss"; | ||||
| @import "./views/elements/_EditableItemList.scss"; | ||||
| @import "./views/elements/_ErrorBoundary.scss"; | ||||
|  |  | |||
|  | @ -64,28 +64,23 @@ limitations under the License. | |||
| } | ||||
| 
 | ||||
| .mx_RoomDirectory_table { | ||||
|     font-size: $font-12px; | ||||
|     color: $primary-fg-color; | ||||
|     width: 100%; | ||||
|     display: grid; | ||||
|     font-size: $font-12px; | ||||
|     grid-template-columns: max-content auto max-content max-content max-content; | ||||
|     row-gap: 24px; | ||||
|     text-align: left; | ||||
|     table-layout: fixed; | ||||
|     width: 100%; | ||||
| } | ||||
| 
 | ||||
| .mx_RoomDirectory_roomAvatar { | ||||
|     width: 32px; | ||||
|     padding-right: 14px; | ||||
|     vertical-align: top; | ||||
| } | ||||
| 
 | ||||
| .mx_RoomDirectory_roomDescription { | ||||
|     padding-bottom: 16px; | ||||
|     padding: 2px 14px 0 0; | ||||
| } | ||||
| 
 | ||||
| .mx_RoomDirectory_roomMemberCount { | ||||
|     align-self: center; | ||||
|     color: $light-fg-color; | ||||
|     width: 60px; | ||||
|     padding: 0 10px; | ||||
|     text-align: center; | ||||
|     padding: 3px 10px 0; | ||||
| 
 | ||||
|     &::before { | ||||
|         background-color: $light-fg-color; | ||||
|  | @ -105,8 +100,7 @@ limitations under the License. | |||
| } | ||||
| 
 | ||||
| .mx_RoomDirectory_join, .mx_RoomDirectory_preview { | ||||
|     width: 80px; | ||||
|     text-align: center; | ||||
|     align-self: center; | ||||
|     white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,72 @@ | |||
| /* | ||||
| 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. | ||||
| */ | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker { | ||||
|     overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_tabLabels { | ||||
|     display: flex; | ||||
|     padding: 0 0 8px 0; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_tabLabel, | ||||
| .mx_desktopCapturerSourcePicker_tabLabel_selected { | ||||
|     width: 100%; | ||||
|     text-align: center; | ||||
|     border-radius: 8px; | ||||
|     padding: 8px 0; | ||||
|     font-size: $font-13px; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_tabLabel_selected { | ||||
|     background-color: $tab-label-active-bg-color; | ||||
|     color: $tab-label-active-fg-color; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_panel { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     justify-content: center; | ||||
|     align-items: flex-start; | ||||
|     height: 500px; | ||||
|     overflow: overlay; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_stream_button { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     margin: 8px; | ||||
|     border-radius: 4px; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_stream_button:hover, | ||||
| .mx_desktopCapturerSourcePicker_stream_button:focus { | ||||
|     background: $roomtile-selected-bg-color; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_stream_thumbnail { | ||||
|     margin: 4px; | ||||
|     width: 312px; | ||||
| } | ||||
| 
 | ||||
| .mx_desktopCapturerSourcePicker_stream_name { | ||||
|     margin: 0 4px; | ||||
|     white-space: nowrap; | ||||
|     text-overflow: ellipsis; | ||||
|     overflow: hidden; | ||||
|     width: 312px; | ||||
| } | ||||
|  | @ -30,7 +30,7 @@ limitations under the License. | |||
|         mask-size: contain; | ||||
|         content: ''; | ||||
|         position: absolute; | ||||
|         top: 2px; | ||||
|         top: 1px; | ||||
|         left: 0; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ $left-gutter: 64px; | |||
| } | ||||
| 
 | ||||
| .mx_EventTile.mx_EventTile_info { | ||||
|     padding-top: 0px; | ||||
|     padding-top: 1px; | ||||
| } | ||||
| 
 | ||||
| .mx_EventTile_avatar { | ||||
|  | @ -37,7 +37,7 @@ $left-gutter: 64px; | |||
| } | ||||
| 
 | ||||
| .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { | ||||
|     top: $font-8px; | ||||
|     top: $font-6px; | ||||
|     left: $left-gutter; | ||||
| } | ||||
| 
 | ||||
|  | @ -420,15 +420,15 @@ $left-gutter: 64px; | |||
| } | ||||
| 
 | ||||
| .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { | ||||
|     border-left: $e2e-verified-color 4px solid; | ||||
|     border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; | ||||
| } | ||||
| 
 | ||||
| .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { | ||||
|     border-left: $e2e-unverified-color 4px solid; | ||||
|     border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; | ||||
| } | ||||
| 
 | ||||
| .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { | ||||
|     border-left: $e2e-unknown-color 4px solid; | ||||
|     border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; | ||||
| } | ||||
| 
 | ||||
| .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, | ||||
|  | @ -446,8 +446,7 @@ $left-gutter: 64px; | |||
| .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, | ||||
| .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, | ||||
| .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { | ||||
|     left: 3px; | ||||
|     width: auto; | ||||
|     width: $MessageTimestamp_width_hover; | ||||
| } | ||||
| 
 | ||||
| // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ $left-gutter: 64px; | |||
| 
 | ||||
|         .mx_MessageTimestamp { | ||||
|             position: absolute; | ||||
|             width: 46px; /* 8 + 30 (avatar) + 8 */ | ||||
|             width: $MessageTimestamp_width; | ||||
|         } | ||||
| 
 | ||||
|         .mx_EventTile_line, .mx_EventTile_reply { | ||||
|  |  | |||
|  | @ -82,6 +82,7 @@ import CountlyAnalytics from "./CountlyAnalytics"; | |||
| import {UIFeature} from "./settings/UIFeature"; | ||||
| import { CallError } from "matrix-js-sdk/src/webrtc/call"; | ||||
| import { logger } from 'matrix-js-sdk/src/logger'; | ||||
| import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker" | ||||
| import { Action } from './dispatcher/actions'; | ||||
| import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper'; | ||||
| import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; | ||||
|  | @ -572,7 +573,15 @@ export default class CallHandler { | |||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|             call.placeScreenSharingCall(remoteElement, localElement); | ||||
| 
 | ||||
|             call.placeScreenSharingCall( | ||||
|                 remoteElement, | ||||
|                 localElement, | ||||
|                 async () : Promise<DesktopCapturerSource> => { | ||||
|                     const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); | ||||
|                     const [source] = await finished; | ||||
|                     return source; | ||||
|                 }); | ||||
|         } else { | ||||
|             console.error("Unknown conf call type: %s", type); | ||||
|         } | ||||
|  |  | |||
|  | @ -202,12 +202,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) { | |||
| } | ||||
| 
 | ||||
| function findOverrideMuteRule(roomId) { | ||||
|     if (!MatrixClientPeg.get().pushRules || | ||||
|         !MatrixClientPeg.get().pushRules['global'] || | ||||
|         !MatrixClientPeg.get().pushRules['global'].override) { | ||||
|     const cli = MatrixClientPeg.get(); | ||||
|     if (!cli.pushRules || | ||||
|         !cli.pushRules['global'] || | ||||
|         !cli.pushRules['global'].override) { | ||||
|         return null; | ||||
|     } | ||||
|     for (const rule of MatrixClientPeg.get().pushRules['global'].override) { | ||||
|     for (const rule of cli.pushRules['global'].override) { | ||||
|         if (isRuleForRoom(roomId, rule)) { | ||||
|             if (isMuteRule(rule) && rule.enabled) { | ||||
|                 return rule; | ||||
|  |  | |||
|  | @ -755,6 +755,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                 break; | ||||
|             case 'on_logged_in': | ||||
|                 if ( | ||||
|                     // Skip this handling for token login as that always calls onLoggedIn itself
 | ||||
|                     !this.tokenLogin && | ||||
|                     !Lifecycle.isSoftLogout() && | ||||
|                     this.state.view !== Views.LOGIN && | ||||
|                     this.state.view !== Views.REGISTER && | ||||
|  | @ -1373,6 +1375,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|         cli.on('Session.logged_out', function(errObj) { | ||||
|             if (Lifecycle.isLoggingOut()) return; | ||||
| 
 | ||||
|             // A modal might have been open when we were logged out by the server
 | ||||
|             Modal.closeCurrentModal('Session.logged_out'); | ||||
| 
 | ||||
|             if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) { | ||||
|                 console.warn("Soft logout issued by server - avoiding data deletion"); | ||||
|                 Lifecycle.softLogout(); | ||||
|  | @ -1383,6 +1388,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|                 title: _t('Signed Out'), | ||||
|                 description: _t('For security, this session has been signed out. Please sign in again.'), | ||||
|             }); | ||||
| 
 | ||||
|             dis.dispatch({ | ||||
|                 action: 'logout', | ||||
|             }); | ||||
|  | @ -1652,10 +1658,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { | |||
|             // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
 | ||||
| 
 | ||||
|             let threepidInvite: IThreepidInvite; | ||||
|             // if we landed here from a 3PID invite, persist it
 | ||||
|             if (params.signurl && params.email) { | ||||
|                 threepidInvite = ThreepidInviteStore.instance | ||||
|                     .storeInvite(roomString, params as IThreepidInviteWireFormat); | ||||
|             } | ||||
|             // otherwise check that this room doesn't already have a known invite
 | ||||
|             if (!threepidInvite) { | ||||
|                 const invites = ThreepidInviteStore.instance.getInvites(); | ||||
|                 threepidInvite = invites.find(invite => invite.roomId === roomString); | ||||
|             } | ||||
| 
 | ||||
|             // on our URLs there might be a ?via=matrix.org or similar to help
 | ||||
|             // joins to the room succeed. We'll pass these through as an array
 | ||||
|  |  | |||
|  | @ -229,7 +229,7 @@ export default class MessagePanel extends React.Component { | |||
| 
 | ||||
|     onAction = (payload) => { | ||||
|         switch (payload.action) { | ||||
|             case "message_sent": | ||||
|             case "scroll_to_bottom": | ||||
|                 this.scrollToBottom(); | ||||
|                 break; | ||||
|         } | ||||
|  |  | |||
|  | @ -30,7 +30,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext"; | |||
| import {Action} from "../../dispatcher/actions"; | ||||
| import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; | ||||
| import WidgetCard from "../views/right_panel/WidgetCard"; | ||||
| import defaultDispatcher from "../../dispatcher/dispatcher"; | ||||
| 
 | ||||
| export default class RightPanel extends React.Component { | ||||
|     static get propTypes() { | ||||
|  | @ -186,7 +185,7 @@ export default class RightPanel extends React.Component { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onCloseUserInfo = () => { | ||||
|     onClose = () => { | ||||
|         // XXX: There are three different ways of 'closing' this panel depending on what state
 | ||||
|         // things are in... this knows far more than it should do about the state of the rest
 | ||||
|         // of the app and is generally a bit silly.
 | ||||
|  | @ -198,31 +197,21 @@ export default class RightPanel extends React.Component { | |||
|             dis.dispatch({ | ||||
|                 action: "view_home_page", | ||||
|             }); | ||||
|         } else if (this.state.phase === RightPanelPhases.EncryptionPanel && | ||||
|         } else if ( | ||||
|             this.state.phase === RightPanelPhases.EncryptionPanel && | ||||
|             this.state.verificationRequest && this.state.verificationRequest.pending | ||||
|         ) { | ||||
|             // When the user clicks close on the encryption panel cancel the pending request first if any
 | ||||
|             this.state.verificationRequest.cancel(); | ||||
|         } else { | ||||
|             // Otherwise we have got our user from RoomViewStore which means we're being shown
 | ||||
|             // within a room/group, so go back to the member panel if we were in the encryption panel,
 | ||||
|             // or the member list if we were in the member panel... phew.
 | ||||
|             const isEncryptionPhase = this.state.phase === RightPanelPhases.EncryptionPanel; | ||||
|             // the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
 | ||||
|             dis.dispatch({ | ||||
|                 action: Action.ViewUser, | ||||
|                 member: isEncryptionPhase ? this.state.member : null, | ||||
|                 action: Action.ToggleRightPanel, | ||||
|                 type: this.props.groupId ? "group" : "room", | ||||
|             }); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     onClose = () => { | ||||
|         // the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
 | ||||
|         defaultDispatcher.dispatch({ | ||||
|             action: Action.ToggleRightPanel, | ||||
|             type: this.props.groupId ? "group" : "room", | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     render() { | ||||
|         const MemberList = sdk.getComponent('rooms.MemberList'); | ||||
|         const UserInfo = sdk.getComponent('right_panel.UserInfo'); | ||||
|  | @ -260,7 +249,7 @@ export default class RightPanel extends React.Component { | |||
|                     user={this.state.member} | ||||
|                     room={this.props.room} | ||||
|                     key={roomId || this.state.member.userId} | ||||
|                     onClose={this.onCloseUserInfo} | ||||
|                     onClose={this.onClose} | ||||
|                     phase={this.state.phase} | ||||
|                     verificationRequest={this.state.verificationRequest} | ||||
|                     verificationRequestPromise={this.state.verificationRequestPromise} | ||||
|  | @ -276,7 +265,7 @@ export default class RightPanel extends React.Component { | |||
|                     user={this.state.member} | ||||
|                     groupId={this.props.groupId} | ||||
|                     key={this.state.member.userId} | ||||
|                     onClose={this.onCloseUserInfo} />; | ||||
|                     onClose={this.onClose} />; | ||||
|                 break; | ||||
| 
 | ||||
|             case RightPanelPhases.GroupRoomInfo: | ||||
|  |  | |||
|  | @ -477,7 +477,7 @@ export default class RoomDirectory extends React.Component { | |||
|         dis.dispatch(payload); | ||||
|     } | ||||
| 
 | ||||
|     getRow(room) { | ||||
|     createRoomCells(room) { | ||||
|         const client = MatrixClientPeg.get(); | ||||
|         const clientRoom = client.getRoom(room.room_id); | ||||
|         const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join"; | ||||
|  | @ -523,31 +523,56 @@ export default class RoomDirectory extends React.Component { | |||
|                                 MatrixClientPeg.get().getHomeserverUrl(), | ||||
|                                 room.avatar_url, 32, 32, "crop", | ||||
|                             ); | ||||
|         return ( | ||||
|             <tr key={ room.room_id } | ||||
|         return [ | ||||
|             <div key={ `${room.room_id}_avatar` } | ||||
|                 onClick={(ev) => this.onRoomClicked(room, ev)} | ||||
|                 // cancel onMouseDown otherwise shift-clicking highlights text
 | ||||
|                 onMouseDown={(ev) => {ev.preventDefault();}} | ||||
|                 className="mx_RoomDirectory_roomAvatar" | ||||
|             > | ||||
|                 <td className="mx_RoomDirectory_roomAvatar"> | ||||
|                     <BaseAvatar width={32} height={32} resizeMethod='crop' | ||||
|                         name={ name } idName={ name } | ||||
|                         url={ avatarUrl } /> | ||||
|                 </td> | ||||
|                 <td className="mx_RoomDirectory_roomDescription"> | ||||
|                     <div className="mx_RoomDirectory_name">{ name }</div>  | ||||
|                     <div className="mx_RoomDirectory_topic" | ||||
|                         onClick={ (ev) => { ev.stopPropagation(); } } | ||||
|                         dangerouslySetInnerHTML={{ __html: topic }} /> | ||||
|                     <div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div> | ||||
|                 </td> | ||||
|                 <td className="mx_RoomDirectory_roomMemberCount"> | ||||
|                     { room.num_joined_members } | ||||
|                 </td> | ||||
|                 <td className="mx_RoomDirectory_preview">{previewButton}</td> | ||||
|                 <td className="mx_RoomDirectory_join">{joinOrViewButton}</td> | ||||
|             </tr> | ||||
|         ); | ||||
|                 <BaseAvatar width={32} height={32} resizeMethod='crop' | ||||
|                     name={ name } idName={ name } | ||||
|                     url={ avatarUrl } | ||||
|                 /> | ||||
|             </div>, | ||||
|             <div key={ `${room.room_id}_description` } | ||||
|                 onClick={(ev) => this.onRoomClicked(room, ev)} | ||||
|                 // cancel onMouseDown otherwise shift-clicking highlights text
 | ||||
|                 onMouseDown={(ev) => {ev.preventDefault();}} | ||||
|                 className="mx_RoomDirectory_roomDescription" | ||||
|             > | ||||
|                 <div className="mx_RoomDirectory_name">{ name }</div>  | ||||
|                 <div className="mx_RoomDirectory_topic" | ||||
|                     onClick={ (ev) => { ev.stopPropagation(); } } | ||||
|                     dangerouslySetInnerHTML={{ __html: topic }} | ||||
|                 /> | ||||
|                 <div className="mx_RoomDirectory_alias">{ get_display_alias_for_room(room) }</div> | ||||
|             </div>, | ||||
|             <div key={ `${room.room_id}_memberCount` } | ||||
|                 onClick={(ev) => this.onRoomClicked(room, ev)} | ||||
|                 // cancel onMouseDown otherwise shift-clicking highlights text
 | ||||
|                 onMouseDown={(ev) => {ev.preventDefault();}} | ||||
|                 className="mx_RoomDirectory_roomMemberCount" | ||||
|             > | ||||
|                 { room.num_joined_members } | ||||
|             </div>, | ||||
|             <div key={ `${room.room_id}_preview` } | ||||
|                 onClick={(ev) => this.onRoomClicked(room, ev)} | ||||
|                 // cancel onMouseDown otherwise shift-clicking highlights text
 | ||||
|                 onMouseDown={(ev) => {ev.preventDefault();}} | ||||
|                 className="mx_RoomDirectory_preview" | ||||
|             > | ||||
|                 {previewButton} | ||||
|             </div>, | ||||
|             <div key={ `${room.room_id}_join` } | ||||
|                 onClick={(ev) => this.onRoomClicked(room, ev)} | ||||
|                 // cancel onMouseDown otherwise shift-clicking highlights text
 | ||||
|                 onMouseDown={(ev) => {ev.preventDefault();}} | ||||
|                 className="mx_RoomDirectory_join" | ||||
|             > | ||||
|                 {joinOrViewButton} | ||||
|             </div>, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     collectScrollPanel = (element) => { | ||||
|  | @ -606,7 +631,8 @@ export default class RoomDirectory extends React.Component { | |||
|         } else if (this.state.protocolsLoading) { | ||||
|             content = <Loader />; | ||||
|         } else { | ||||
|             const rows = (this.state.publicRooms || []).map(room => this.getRow(room)); | ||||
|             const cells = (this.state.publicRooms || []) | ||||
|                 .reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],); | ||||
|             // we still show the scrollpanel, at least for now, because
 | ||||
|             // otherwise we don't fetch more because we don't get a fill
 | ||||
|             // request from the scrollpanel because there isn't one
 | ||||
|  | @ -617,14 +643,12 @@ export default class RoomDirectory extends React.Component { | |||
|             } | ||||
| 
 | ||||
|             let scrollpanel_content; | ||||
|             if (rows.length === 0 && !this.state.loading) { | ||||
|             if (cells.length === 0 && !this.state.loading) { | ||||
|                 scrollpanel_content = <i>{ _t('No rooms to show') }</i>; | ||||
|             } else { | ||||
|                 scrollpanel_content = <table className="mx_RoomDirectory_table"> | ||||
|                     <tbody> | ||||
|                         { rows } | ||||
|                     </tbody> | ||||
|                 </table>; | ||||
|                 scrollpanel_content = <div className="mx_RoomDirectory_table"> | ||||
|                     { cells } | ||||
|                 </div>; | ||||
|             } | ||||
|             const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); | ||||
|             content = <ScrollPanel ref={this.collectScrollPanel} | ||||
|  |  | |||
|  | @ -0,0 +1,166 @@ | |||
| /* | ||||
| 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 { _t } from '../../../languageHandler'; | ||||
| import BaseDialog from "..//dialogs/BaseDialog" | ||||
| import AccessibleButton from './AccessibleButton'; | ||||
| import {getDesktopCapturerSources} from "matrix-js-sdk/src/webrtc/call"; | ||||
| 
 | ||||
| export interface DesktopCapturerSource { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     thumbnailURL; | ||||
| } | ||||
| 
 | ||||
| export enum Tabs { | ||||
|     Screens = "screens", | ||||
|     Windows = "windows", | ||||
| } | ||||
| 
 | ||||
| export interface DesktopCapturerSourceIProps { | ||||
|     source: DesktopCapturerSource; | ||||
|     onSelect(source: DesktopCapturerSource): void; | ||||
| } | ||||
| 
 | ||||
| export class ExistingSource extends React.Component<DesktopCapturerSourceIProps> { | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
|     } | ||||
| 
 | ||||
|     onClick = (ev) => { | ||||
|         this.props.onSelect(this.props.source); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         return ( | ||||
|             <AccessibleButton | ||||
|                 className="mx_desktopCapturerSourcePicker_stream_button" | ||||
|                 title={this.props.source.name} | ||||
|                 onClick={this.onClick} > | ||||
|                 <img | ||||
|                     className="mx_desktopCapturerSourcePicker_stream_thumbnail" | ||||
|                     src={this.props.source.thumbnailURL} | ||||
|                 /> | ||||
|                 <span className="mx_desktopCapturerSourcePicker_stream_name">{this.props.source.name}</span> | ||||
|             </AccessibleButton> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export interface DesktopCapturerSourcePickerIState { | ||||
|     selectedTab: Tabs; | ||||
|     sources: Array<DesktopCapturerSource>; | ||||
| } | ||||
| export interface DesktopCapturerSourcePickerIProps { | ||||
|     onFinished(source: DesktopCapturerSource): void; | ||||
| } | ||||
| 
 | ||||
| export default class DesktopCapturerSourcePicker extends React.Component< | ||||
|     DesktopCapturerSourcePickerIProps, | ||||
|     DesktopCapturerSourcePickerIState | ||||
|     > { | ||||
|     interval; | ||||
| 
 | ||||
|     constructor(props) { | ||||
|         super(props); | ||||
| 
 | ||||
|         this.state = { | ||||
|             selectedTab: Tabs.Screens, | ||||
|             sources: [], | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     componentDidMount() { | ||||
|         // We update the sources every 500ms to get newer thumbnails
 | ||||
|         this.interval = setInterval(async () => { | ||||
|             this.setState({ | ||||
|                 sources: await getDesktopCapturerSources(), | ||||
|             }); | ||||
|         }, 500); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         clearInterval(this.interval); | ||||
|     } | ||||
| 
 | ||||
|     onSelect = (source) => { | ||||
|         this.props.onFinished(source); | ||||
|     } | ||||
| 
 | ||||
|     onScreensClick = (ev) => { | ||||
|         this.setState({selectedTab: Tabs.Screens}); | ||||
|     } | ||||
| 
 | ||||
|     onWindowsClick = (ev) => { | ||||
|         this.setState({selectedTab: Tabs.Windows}); | ||||
|     } | ||||
| 
 | ||||
|     onCloseClick = (ev) => { | ||||
|         this.props.onFinished(null); | ||||
|     } | ||||
| 
 | ||||
|     render() { | ||||
|         let sources; | ||||
|         if (this.state.selectedTab === Tabs.Screens) { | ||||
|             sources = this.state.sources | ||||
|                 .filter((source) => { | ||||
|                     return source.id.startsWith("screen"); | ||||
|                 }) | ||||
|                 .map((source) => { | ||||
|                     return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />; | ||||
|                 }); | ||||
|         } else { | ||||
|             sources = this.state.sources | ||||
|                 .filter((source) => { | ||||
|                     return source.id.startsWith("window"); | ||||
|                 }) | ||||
|                 .map((source) => { | ||||
|                     return <ExistingSource source={source} onSelect={this.onSelect} key={source.id} />; | ||||
|                 }); | ||||
|         } | ||||
| 
 | ||||
|         const buttonStyle = "mx_desktopCapturerSourcePicker_tabLabel"; | ||||
|         const screensButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Screens) ? "_selected" : ""); | ||||
|         const windowsButtonStyle = buttonStyle + ((this.state.selectedTab === Tabs.Windows) ? "_selected" : ""); | ||||
| 
 | ||||
|         return ( | ||||
|             <BaseDialog | ||||
|                 className="mx_desktopCapturerSourcePicker" | ||||
|                 onFinished={this.onCloseClick} | ||||
|                 title={_t("Share your screen")} | ||||
|             > | ||||
|                 <div className="mx_desktopCapturerSourcePicker_tabLabels"> | ||||
|                     <AccessibleButton | ||||
|                         className={screensButtonStyle} | ||||
|                         onClick={this.onScreensClick} | ||||
|                     > | ||||
|                         {_t("Screens")} | ||||
|                     </AccessibleButton> | ||||
|                     <AccessibleButton | ||||
|                         className={windowsButtonStyle} | ||||
|                         onClick={this.onWindowsClick} | ||||
|                     > | ||||
|                         {_t("Windows")} | ||||
|                     </AccessibleButton> | ||||
|                 </div> | ||||
|                 <div className="mx_desktopCapturerSourcePicker_panel"> | ||||
|                     { sources } | ||||
|                 </div> | ||||
|             </BaseDialog> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -23,6 +23,7 @@ import ResizeObserver from 'resize-observer-polyfill'; | |||
| import dis from '../../../dispatcher/dispatcher'; | ||||
| import MatrixClientContext from "../../../contexts/MatrixClientContext"; | ||||
| import {MatrixClientPeg} from "../../../MatrixClientPeg"; | ||||
| import {isNullOrUndefined} from "matrix-js-sdk/src/utils"; | ||||
| 
 | ||||
| // Shamelessly ripped off Modal.js.  There's probably a better way
 | ||||
| // of doing reusable widgets like dialog boxes & menus where we go and
 | ||||
|  | @ -61,6 +62,9 @@ export default class PersistedElement extends React.Component { | |||
|         // Any PersistedElements with the same persistKey will use
 | ||||
|         // the same DOM container.
 | ||||
|         persistKey: PropTypes.string.isRequired, | ||||
| 
 | ||||
|         // z-index for the element. Defaults to 9.
 | ||||
|         zIndex: PropTypes.number, | ||||
|     }; | ||||
| 
 | ||||
|     constructor() { | ||||
|  | @ -165,7 +169,7 @@ export default class PersistedElement extends React.Component { | |||
| 
 | ||||
|         const parentRect = parent.getBoundingClientRect(); | ||||
|         Object.assign(child.style, { | ||||
|             zIndex: 9, | ||||
|             zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex, | ||||
|             position: 'absolute', | ||||
|             top: parentRect.top + 'px', | ||||
|             left: parentRect.left + 'px', | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ interface IProps { | |||
|     previousPhase?: RightPanelPhases; | ||||
|     closeLabel?: string; | ||||
|     onClose?(): void; | ||||
|     refireParams?; | ||||
| } | ||||
| 
 | ||||
| interface IGroupProps { | ||||
|  | @ -56,6 +57,7 @@ const BaseCard: React.FC<IProps> = ({ | |||
|     withoutScrollContainer, | ||||
|     previousPhase, | ||||
|     children, | ||||
|     refireParams, | ||||
| }) => { | ||||
|     let backButton; | ||||
|     if (previousPhase) { | ||||
|  | @ -63,6 +65,7 @@ const BaseCard: React.FC<IProps> = ({ | |||
|             defaultDispatcher.dispatch<SetRightPanelPhasePayload>({ | ||||
|                 action: Action.SetRightPanelPhase, | ||||
|                 phase: previousPhase, | ||||
|                 refireParams: refireParams, | ||||
|             }); | ||||
|         }; | ||||
|         backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />; | ||||
|  |  | |||
|  | @ -60,6 +60,7 @@ import QuestionDialog from "../dialogs/QuestionDialog"; | |||
| import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; | ||||
| import InfoDialog from "../dialogs/InfoDialog"; | ||||
| import { EventType } from "matrix-js-sdk/src/@types/event"; | ||||
| import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; | ||||
| 
 | ||||
| interface IDevice { | ||||
|     deviceId: string; | ||||
|  | @ -1534,6 +1535,24 @@ const UserInfo: React.FC<Props> = ({ | |||
| 
 | ||||
|     const classes = ["mx_UserInfo"]; | ||||
| 
 | ||||
|     let refireParams; | ||||
|     let previousPhase: RightPanelPhases; | ||||
|     // We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
 | ||||
|     if (room && phase === RightPanelPhases.EncryptionPanel) { | ||||
|         previousPhase = RightPanelPhases.RoomMemberInfo; | ||||
|         refireParams = {member: member}; | ||||
|     } else if (room) { | ||||
|         previousPhase = RightPanelPhases.RoomMemberList; | ||||
|     } | ||||
| 
 | ||||
|     const onEncryptionPanelClose = () => { | ||||
|         dis.dispatch<SetRightPanelPhasePayload>({ | ||||
|             action: Action.SetRightPanelPhase, | ||||
|             phase: previousPhase, | ||||
|             refireParams: refireParams, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     let content; | ||||
|     switch (phase) { | ||||
|         case RightPanelPhases.RoomMemberInfo: | ||||
|  | @ -1553,19 +1572,13 @@ const UserInfo: React.FC<Props> = ({ | |||
|                 <EncryptionPanel | ||||
|                     {...props as React.ComponentProps<typeof EncryptionPanel>} | ||||
|                     member={member} | ||||
|                     onClose={onClose} | ||||
|                     onClose={onEncryptionPanelClose} | ||||
|                     isRoomEncrypted={isRoomEncrypted} | ||||
|                 /> | ||||
|             ); | ||||
|             break; | ||||
|     } | ||||
| 
 | ||||
|     let previousPhase: RightPanelPhases; | ||||
|     // We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
 | ||||
|     if (room) { | ||||
|         previousPhase = RightPanelPhases.RoomMemberList; | ||||
|     } | ||||
| 
 | ||||
|     let closeLabel = undefined; | ||||
|     if (phase === RightPanelPhases.EncryptionPanel) { | ||||
|         const verificationRequest = (props as React.ComponentProps<typeof EncryptionPanel>).verificationRequest; | ||||
|  | @ -1581,6 +1594,7 @@ const UserInfo: React.FC<Props> = ({ | |||
|         onClose={onClose} | ||||
|         closeLabel={closeLabel} | ||||
|         previousPhase={previousPhase} | ||||
|         refireParams={refireParams} | ||||
|     > | ||||
|         { content } | ||||
|     </BaseCard>; | ||||
|  |  | |||
|  | @ -69,19 +69,24 @@ export default class RoomProfileSettings extends React.Component { | |||
|         // clear file upload field so same file can be selected
 | ||||
|         this._avatarUpload.current.value = ""; | ||||
|         this.setState({ | ||||
|             avatarUrl: undefined, | ||||
|             avatarFile: undefined, | ||||
|             avatarUrl: null, | ||||
|             avatarFile: null, | ||||
|             enableProfileSave: true, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _clearProfile = async (e) => { | ||||
|     _cancelProfileChanges = async (e) => { | ||||
|         e.stopPropagation(); | ||||
|         e.preventDefault(); | ||||
| 
 | ||||
|         if (!this.state.enableProfileSave) return; | ||||
|         this._removeAvatar(); | ||||
|         this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName}); | ||||
|         this.setState({ | ||||
|             enableProfileSave: false, | ||||
|             displayName: this.state.originalDisplayName, | ||||
|             topic: this.state.originalTopic, | ||||
|             avatarUrl: this.state.originalAvatarUrl, | ||||
|             avatarFile: null, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _saveProfile = async (e) => { | ||||
|  | @ -108,7 +113,7 @@ export default class RoomProfileSettings extends React.Component { | |||
|             newState.originalAvatarUrl = newState.avatarUrl; | ||||
|             newState.avatarFile = null; | ||||
|         } else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { | ||||
|             await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {url: undefined}, ''); | ||||
|             await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, ''); | ||||
|         } | ||||
| 
 | ||||
|         if (this.state.originalTopic !== this.state.topic) { | ||||
|  | @ -164,11 +169,15 @@ export default class RoomProfileSettings extends React.Component { | |||
|         const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); | ||||
| 
 | ||||
|         let profileSettingsButtons; | ||||
|         if (this.state.canSetTopic && this.state.canSetName) { | ||||
|         if ( | ||||
|             this.state.canSetName || | ||||
|             this.state.canSetTopic || | ||||
|             this.state.canSetAvatar | ||||
|         ) { | ||||
|             profileSettingsButtons = ( | ||||
|                 <div className="mx_ProfileSettings_buttons"> | ||||
|                     <AccessibleButton | ||||
|                         onClick={this._clearProfile} | ||||
|                         onClick={this._cancelProfileChanges} | ||||
|                         kind="link" | ||||
|                         disabled={!this.state.enableProfileSave} | ||||
|                     > | ||||
|  |  | |||
|  | @ -426,7 +426,8 @@ export default class MessageComposer extends React.Component { | |||
|                 <EmojiButton key="emoji_button" addEmoji={this.addEmoji} />, | ||||
|             ); | ||||
| 
 | ||||
|             if (SettingsStore.getValue(UIFeature.Widgets)) { | ||||
|             if (SettingsStore.getValue(UIFeature.Widgets) && | ||||
|                 SettingsStore.getValue("MessageComposerInput.showStickersButton")) { | ||||
|                 controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />); | ||||
|             } | ||||
| 
 | ||||
|  |  | |||
|  | @ -403,6 +403,7 @@ export default class SendMessageComposer extends React.Component { | |||
|         this._editorRef.clearUndoHistory(); | ||||
|         this._editorRef.focus(); | ||||
|         this._clearStoredEditorState(); | ||||
|         dis.dispatch({action: "scroll_to_bottom"}); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|  |  | |||
|  | @ -264,7 +264,7 @@ export default class Stickerpicker extends React.Component { | |||
|                             width: this.popoverWidth, | ||||
|                         }} | ||||
|                     > | ||||
|                     <PersistedElement persistKey={PERSISTED_ELEMENT_KEY} style={{zIndex: STICKERPICKER_Z_INDEX}}> | ||||
|                     <PersistedElement persistKey={PERSISTED_ELEMENT_KEY} zIndex={STICKERPICKER_Z_INDEX}> | ||||
|                         <AppTile | ||||
|                             app={stickerApp} | ||||
|                             room={this.props.room} | ||||
|  |  | |||
|  | @ -52,19 +52,23 @@ export default class ProfileSettings extends React.Component { | |||
|         // clear file upload field so same file can be selected
 | ||||
|         this._avatarUpload.current.value = ""; | ||||
|         this.setState({ | ||||
|             avatarUrl: undefined, | ||||
|             avatarFile: undefined, | ||||
|             avatarUrl: null, | ||||
|             avatarFile: null, | ||||
|             enableProfileSave: true, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _clearProfile = async (e) => { | ||||
|     _cancelProfileChanges = async (e) => { | ||||
|         e.stopPropagation(); | ||||
|         e.preventDefault(); | ||||
| 
 | ||||
|         if (!this.state.enableProfileSave) return; | ||||
|         this._removeAvatar(); | ||||
|         this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName}); | ||||
|         this.setState({ | ||||
|             enableProfileSave: false, | ||||
|             displayName: this.state.originalDisplayName, | ||||
|             avatarUrl: this.state.originalAvatarUrl, | ||||
|             avatarFile: null, | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|     _saveProfile = async (e) => { | ||||
|  | @ -186,7 +190,7 @@ export default class ProfileSettings extends React.Component { | |||
|                 </div> | ||||
|                 <div className="mx_ProfileSettings_buttons"> | ||||
|                     <AccessibleButton | ||||
|                         onClick={this._clearProfile} | ||||
|                         onClick={this._cancelProfileChanges} | ||||
|                         kind="link" | ||||
|                         disabled={!this.state.enableProfileSave} | ||||
|                     > | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component { | |||
|         'MessageComposerInput.suggestEmoji', | ||||
|         'sendTypingNotifications', | ||||
|         'MessageComposerInput.ctrlEnterToSend', | ||||
|         'MessageComposerInput.showStickersButton', | ||||
|     ]; | ||||
| 
 | ||||
|     static TIMELINE_SETTINGS = [ | ||||
|  |  | |||
|  | @ -795,6 +795,7 @@ | |||
|     "Font size": "Font size", | ||||
|     "Use custom size": "Use custom size", | ||||
|     "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", | ||||
|     "Show stickers button": "Show stickers button", | ||||
|     "Use a more compact ‘Modern’ layout": "Use a more compact ‘Modern’ layout", | ||||
|     "Show a placeholder for removed messages": "Show a placeholder for removed messages", | ||||
|     "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", | ||||
|  | @ -1851,6 +1852,9 @@ | |||
|     "Use the <a>Desktop app</a> to search encrypted messages": "Use the <a>Desktop app</a> to search encrypted messages", | ||||
|     "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", | ||||
|     "This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages", | ||||
|     "Share your screen": "Share your screen", | ||||
|     "Screens": "Screens", | ||||
|     "Windows": "Windows", | ||||
|     "Join": "Join", | ||||
|     "No results": "No results", | ||||
|     "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", | ||||
|  |  | |||
|  | @ -240,6 +240,11 @@ export const SETTINGS: {[setting: string]: ISetting} = { | |||
|         default: true, | ||||
|         invertedSettingName: 'MessageComposerInput.dontSuggestEmoji', | ||||
|     }, | ||||
|     "MessageComposerInput.showStickersButton": { | ||||
|         supportedLevels: LEVELS_ACCOUNT_SETTINGS, | ||||
|         displayName: _td('Show stickers button'), | ||||
|         default: true, | ||||
|     }, | ||||
|     // TODO: Wire up appropriately to UI (FTUE notifications)
 | ||||
|     "Notifications.alwaysShowBadgeCounts": { | ||||
|         supportedLevels: LEVELS_ROOM_OR_ACCOUNT, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Šimon Brandner
						Šimon Brandner