From c820836bcc1c95d0fb292f47ecb377e602f5b9ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Jun 2018 15:22:45 +0100 Subject: [PATCH 1/5] make RoomTooltip generic and add ContextMenu&Tooltip to GroupInviteTile Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../GroupInviteTileContextMenu.js | 87 ++++++++++++++++ .../views/groups/GroupInviteTile.js | 98 +++++++++++++++++-- src/components/views/rooms/RoomList.js | 3 +- src/components/views/rooms/RoomTile.js | 4 +- src/components/views/rooms/RoomTooltip.js | 25 ++--- 5 files changed, 189 insertions(+), 28 deletions(-) create mode 100644 src/components/views/context_menus/GroupInviteTileContextMenu.js diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js new file mode 100644 index 0000000000..844845ea82 --- /dev/null +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -0,0 +1,87 @@ +/* +Copyright 2018 Vector Creations 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 React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import { _t, _td } from '../../../languageHandler'; +import Modal from '../../../Modal'; +import {Group} from 'matrix-js-sdk'; +import GroupStore from "../../../stores/GroupStore"; + +export default class GroupInviteTileContextMenu extends React.Component { + static propTypes = { + group: PropTypes.instanceOf(Group).isRequired, + /* callback called when the menu is dismissed */ + onFinished: PropTypes.func, + }; + + constructor(props, context) { + super(props, context); + + this._onClickReject = this._onClickReject.bind(this); + } + + componentWillMount() { + this._unmounted = false; + } + + componentWillUnmount() { + this._unmounted = true; + } + + _onClickReject() { + const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); + Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, { + title: _t('Reject invitation'), + description: _t('Are you sure you want to reject the invitation?'), + onFinished: async (shouldLeave) => { + if (!shouldLeave) return; + + // FIXME: controller shouldn't be loading a view :( + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + + try { + await GroupStore.leaveGroup(this.props.group.groupId); + } catch (e) { + console.error(e); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, { + title: _t("Error"), + description: _t("Unable to reject invite"), + }); + } + modal.close(); + }, + }); + + // Close the context menu + if (this.props.onFinished) { + this.props.onFinished(); + } + } + + render() { + return
+
+ + { _t('Reject') } +
+
; + } +} diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index d97464e8ca..65e8a07d5a 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -1,5 +1,5 @@ /* -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 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. @@ -20,6 +20,8 @@ import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; +import * as ContextualMenu from "../../structures/ContextualMenu"; +import classNames from 'classnames'; export default React.createClass({ displayName: 'GroupInviteTile', @@ -32,6 +34,15 @@ export default React.createClass({ matrixClient: PropTypes.instanceOf(MatrixClient), }, + getInitialState: function() { + return ({ + hover: false, + badgeHover: false, + menuDisplayed: false, + selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state + }); + }, + onClick: function(e) { dis.dispatch({ action: 'view_group', @@ -39,6 +50,56 @@ export default React.createClass({ }); }, + onMouseEnter: function() { + const state = {hover: true}; + // Only allow non-guests to access the context menu + if (!this.context.matrixClient.isGuest()) { + state.badgeHover = true; + } + this.setState(state); + }, + + onMouseLeave: function() { + this.setState({ + badgeHover: false, + hover: false, + }); + }, + + onBadgeClicked: function(e) { + // Prevent the RoomTile onClick event firing as well + e.stopPropagation(); + + // Only allow none guests to access the context menu + if (this.context.matrixClient.isGuest()) return; + + // If the badge is clicked, then no longer show tooltip + if (this.props.collapsed) { + this.setState({ hover: false }); + } + + const RoomTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu'); + const elementRect = e.target.getBoundingClientRect(); + + // The window X and Y offsets are to adjust position when zoomed in to page + const x = elementRect.right + window.pageXOffset + 3; + const chevronOffset = 12; + let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset); + y = y - (chevronOffset + 8); // where 8 is half the height of the chevron + + ContextualMenu.createMenu(RoomTileContextMenu, { + chevronOffset: chevronOffset, + left: x, + top: y, + group: this.props.group, + onFinished: () => { + this.setState({ menuDisplayed: false }); + // this.props.refreshSubList(); + }, + }); + this.setState({ menuDisplayed: true }); + }, + render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const EmojiText = sdk.getComponent('elements.EmojiText'); @@ -49,19 +110,37 @@ export default React.createClass({ const av = ; - const label = + const nameClasses = classNames({ + 'mx_RoomTile_name': true, + 'mx_RoomTile_invite': this.props.isInvite, + 'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed, + }); + + const label = { groupName } ; - const badge =
!
; + const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed; + const badgeClasses = classNames('mx_RoomSubList_badge mx_RoomSubList_badgeHighlight', { + 'mx_RoomTile_badgeButton': badgeEllipsis, + }); + + const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; + const badge =
{ badgeContent }
; + + let tooltip; + if (this.props.collapsed && this.state.hover) { + const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); + tooltip = ; + } + + const classes = classNames('mx_RoomTile mx_RoomTile_highlight', { + 'mx_RoomTile_menuDisplayed': this.state.menuDisplayed, + 'mx_RoomTile_selected': this.state.selected, + }); return ( - +
{ av }
@@ -69,6 +148,7 @@ export default React.createClass({ { label } { badge } + { tooltip }
); }, diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index fc1872249f..2722bad88b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -589,8 +589,7 @@ module.exports = React.createClass({ const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile'); for (const group of MatrixClientPeg.get().getGroups()) { if (group.myMembership !== 'invite') continue; - - ret.push(); + ret.push(); } return ret; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 05aaf79e0b..11eb2090f2 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -301,7 +301,7 @@ module.exports = React.createClass({ } } else if (this.state.hover) { const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); - tooltip = ; + tooltip = ; } //var incomingCallBox; @@ -314,7 +314,7 @@ module.exports = React.createClass({ let directMessageIndicator; if (this._isDirectMessageRoom(this.props.room.roomId)) { - directMessageIndicator = dm; + directMessageIndicator = dm; } return diff --git a/src/components/views/rooms/RoomTooltip.js b/src/components/views/rooms/RoomTooltip.js index b17f54ef3c..bce0922637 100644 --- a/src/components/views/rooms/RoomTooltip.js +++ b/src/components/views/rooms/RoomTooltip.js @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); -var dis = require('../../../dispatcher'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import dis from '../../../dispatcher'; import classNames from 'classnames'; const MIN_TOOLTIP_HEIGHT = 25; @@ -77,25 +76,21 @@ module.exports = React.createClass({ }, _renderTooltip: function() { - var label = this.props.room ? this.props.room.name : this.props.label; - // Add the parent's position to the tooltips, so it's correctly // positioned, also taking into account any window zoom // NOTE: The additional 6 pixels for the left position, is to take account of the // tooltips chevron - var parent = ReactDOM.findDOMNode(this).parentNode; - var style = {}; + const parent = ReactDOM.findDOMNode(this).parentNode; + let style = {}; style = this._updatePosition(style); style.display = "block"; - const tooltipClasses = classNames( - "mx_RoomTooltip", this.props.tooltipClassName, - ); + const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName); - var tooltip = ( -
-
- { label } + const tooltip = ( +
+
+ { this.props.label }
); From 9dd2184b33720b44c34b29f8039652df02eb66ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 12 Jun 2018 15:29:05 +0100 Subject: [PATCH 2/5] remove unused imports Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/context_menus/GroupInviteTileContextMenu.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index 844845ea82..bbdc3a4244 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -15,10 +15,9 @@ limitations under the License. */ import React from 'react'; -import classNames from 'classnames'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import { _t, _td } from '../../../languageHandler'; +import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import {Group} from 'matrix-js-sdk'; import GroupStore from "../../../stores/GroupStore"; From 4e216714b533722e4c0b4bef3f0068a0d0bccb91 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Jun 2018 13:53:02 +0100 Subject: [PATCH 3/5] remove commented code and move modal.close into a finally block Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/context_menus/GroupInviteTileContextMenu.js | 5 +++-- src/components/views/groups/GroupInviteTile.js | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index bbdc3a4244..e30acca16d 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -58,14 +58,15 @@ export default class GroupInviteTileContextMenu extends React.Component { try { await GroupStore.leaveGroup(this.props.group.groupId); } catch (e) { - console.error(e); + console.error("Error rejecting community invite: ", e); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, { title: _t("Error"), description: _t("Unable to reject invite"), }); + } finally { + modal.close(); } - modal.close(); }, }); diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 65e8a07d5a..4d5f3c6f3a 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -94,7 +94,6 @@ export default React.createClass({ group: this.props.group, onFinished: () => { this.setState({ menuDisplayed: false }); - // this.props.refreshSubList(); }, }); this.setState({ menuDisplayed: true }); From 1346f47a12a2850b64e9555e999c8f3faab480d4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Jun 2018 14:18:39 +0100 Subject: [PATCH 4/5] allow filtering Group Invite Tiles with the same search filter Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2722bad88b..c329e3f2a0 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -583,13 +583,18 @@ module.exports = React.createClass({ } }, - _makeGroupInviteTiles() { + _makeGroupInviteTiles(filter) { const ret = []; + const lcFilter = filter && filter.toLowerCase(); const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile'); for (const group of MatrixClientPeg.get().getGroups()) { - if (group.myMembership !== 'invite') continue; - ret.push(); + const {groupId: id, name, myMembership: membership} = group; + // filter to only groups in invite state and group_id starts with filter or group name includes it + if (membership !== 'invite') continue; + if (lcFilter && !id.toLowerCase().startsWith(lcFilter) && + !(name && name.toLowerCase().includes(lcFilter))) continue; + ret.push(); } return ret; @@ -609,7 +614,7 @@ module.exports = React.createClass({ autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
Date: Thu, 14 Jun 2018 16:48:00 +0100 Subject: [PATCH 5/5] change variable names for clarity Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index c329e3f2a0..167603ecfb 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -589,12 +589,12 @@ module.exports = React.createClass({ const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile'); for (const group of MatrixClientPeg.get().getGroups()) { - const {groupId: id, name, myMembership: membership} = group; + const {groupId, name, myMembership} = group; // filter to only groups in invite state and group_id starts with filter or group name includes it - if (membership !== 'invite') continue; - if (lcFilter && !id.toLowerCase().startsWith(lcFilter) && + if (myMembership !== 'invite') continue; + if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) && !(name && name.toLowerCase().includes(lcFilter))) continue; - ret.push(); + ret.push(); } return ret;