Implement TagPanel (or LeftLeftPanel) for group filtering
This allows for filtering of the RoomList by group. When a group is selected, the room list will show: - Rooms in the group - Direct messages with members in the group A button at the bottom of the TagPanel allows for creating new groups, which will appear in the panel following creation.pull/21833/head
							parent
							
								
									ff25c2f329
								
							
						
					
					
						commit
						45bcb6f2ed
					
				|  | @ -213,6 +213,7 @@ export default React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         const TagPanel = sdk.getComponent('structures.TagPanel'); | ||||
|         const LeftPanel = sdk.getComponent('structures.LeftPanel'); | ||||
|         const RightPanel = sdk.getComponent('structures.RightPanel'); | ||||
|         const RoomView = sdk.getComponent('structures.RoomView'); | ||||
|  | @ -334,6 +335,7 @@ export default React.createClass({ | |||
|             <div className='mx_MatrixChat_wrapper'> | ||||
|                 { topBar } | ||||
|                 <div className={bodyClasses}> | ||||
|                     <TagPanel /> | ||||
|                     <LeftPanel | ||||
|                         selectedRoom={this.props.currentRoomId} | ||||
|                         collapsed={this.props.collapseLhs || false} | ||||
|  |  | |||
|  | @ -0,0 +1,173 @@ | |||
| /* | ||||
| 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 React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { MatrixClient } from 'matrix-js-sdk'; | ||||
| import classNames from 'classnames'; | ||||
| import FilterStore from '../../stores/FilterStore'; | ||||
| import FlairStore from '../../stores/FlairStore'; | ||||
| import sdk from '../../index'; | ||||
| import dis from '../../dispatcher'; | ||||
| 
 | ||||
| const TagTile = React.createClass({ | ||||
|     displayName: 'TagTile', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         groupProfile: PropTypes.object, | ||||
|     }, | ||||
| 
 | ||||
|     contextTypes: { | ||||
|         matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState() { | ||||
|         return { | ||||
|             hover: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onClick: function(e) { | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|         dis.dispatch({ | ||||
|             action: 'view_group', | ||||
|             group_id: this.props.groupProfile.groupId, | ||||
|         }); | ||||
|         dis.dispatch({ | ||||
|             action: 'select_tag', | ||||
|             tag: this.props.groupProfile.groupId, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onMouseOver: function() { | ||||
|         this.setState({hover: true}); | ||||
|     }, | ||||
| 
 | ||||
|     onMouseOut: function() { | ||||
|         this.setState({hover: false}); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); | ||||
|         const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); | ||||
|         const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); | ||||
|         const profile = this.props.groupProfile || {}; | ||||
|         const name = profile.name || profile.groupId; | ||||
|         const avatarHeight = 35; | ||||
| 
 | ||||
|         const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( | ||||
|             profile.avatarUrl, avatarHeight, avatarHeight, "crop", | ||||
|         ) : null; | ||||
| 
 | ||||
|         const className = classNames({ | ||||
|             mx_TagTile: true, | ||||
|             mx_TagTile_selected: this.props.selected, | ||||
|         }); | ||||
| 
 | ||||
|         const tip = this.state.hover ? | ||||
|             <RoomTooltip className="mx_TagTile_tooltip" label={name} /> : | ||||
|             <div />; | ||||
|         return <AccessibleButton className={className} onClick={this.onClick}> | ||||
|             <div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}> | ||||
|                 <BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} /> | ||||
|                 { tip } | ||||
|             </div> | ||||
|         </AccessibleButton>; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| export default React.createClass({ | ||||
|     displayName: 'TagPanel', | ||||
| 
 | ||||
|     contextTypes: { | ||||
|         matrixClient: PropTypes.instanceOf(MatrixClient), | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState() { | ||||
|         return { | ||||
|             joinedGroupProfiles: [], | ||||
|             selectedTags: [], | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         this.mounted = true; | ||||
|         this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); | ||||
| 
 | ||||
|         this._filterStoreToken = FilterStore.addListener(() => { | ||||
|             if (!this.mounted) { | ||||
|                 return; | ||||
|             } | ||||
|             this.setState({ | ||||
|                 selectedTags: FilterStore.getSelectedTags(), | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         this._fetchJoinedRooms(); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|         this.mounted = false; | ||||
|         this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); | ||||
|         if (this._filterStoreToken) { | ||||
|             this._filterStoreToken.remove(); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _onGroupMyMembership() { | ||||
|         if (!this.mounted) return; | ||||
|         this._fetchJoinedRooms(); | ||||
|     }, | ||||
| 
 | ||||
|     onClick() { | ||||
|         dis.dispatch({action: 'deselect_tags'}); | ||||
|     }, | ||||
| 
 | ||||
|     onCreateGroupClick(ev) { | ||||
|         ev.stopPropagation(); | ||||
|         dis.dispatch({action: 'view_create_group'}); | ||||
|     }, | ||||
| 
 | ||||
|     async _fetchJoinedRooms() { | ||||
|         const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups(); | ||||
|         const joinedGroupIds = joinedGroupResponse.groups; | ||||
|         const joinedGroupProfiles = await Promise.all(joinedGroupIds.map( | ||||
|             (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), | ||||
|         )); | ||||
|         this.setState({joinedGroupProfiles}); | ||||
|     }, | ||||
| 
 | ||||
|     render() { | ||||
|         const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); | ||||
|         const TintableSvg = sdk.getComponent('elements.TintableSvg'); | ||||
|         const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => { | ||||
|             return <TagTile | ||||
|                 key={groupProfile.groupId + '_' + index} | ||||
|                 groupProfile={groupProfile} | ||||
|                 selected={this.state.selectedTags.includes(groupProfile.groupId)} | ||||
|             />; | ||||
|         }); | ||||
|         return <div className="mx_TagPanel" onClick={this.onClick}> | ||||
|             <div className="mx_TagPanel_tagTileContainer"> | ||||
|                 { tags } | ||||
|             </div> | ||||
|             <AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}> | ||||
|                 <TintableSvg src="img/icons-create-room.svg" width="25" height="25" /> | ||||
|             </AccessibleButton> | ||||
|         </div>; | ||||
|     }, | ||||
| }); | ||||
|  | @ -28,6 +28,8 @@ const rate_limited_func = require('../../../ratelimitedfunc'); | |||
| const Rooms = require('../../../Rooms'); | ||||
| import DMRoomMap from '../../../utils/DMRoomMap'; | ||||
| const Receipt = require('../../../utils/Receipt'); | ||||
| import FilterStore from '../../../stores/FilterStore'; | ||||
| import GroupStoreCache from '../../../stores/GroupStoreCache'; | ||||
| 
 | ||||
| const HIDE_CONFERENCE_CHANS = true; | ||||
| 
 | ||||
|  | @ -61,6 +63,7 @@ module.exports = React.createClass({ | |||
|             totalRoomCount: null, | ||||
|             lists: {}, | ||||
|             incomingCall: null, | ||||
|             selectedTags: [], | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|  | @ -80,6 +83,23 @@ module.exports = React.createClass({ | |||
|         cli.on("accountData", this.onAccountData); | ||||
|         cli.on("Group.myMembership", this._onGroupMyMembership); | ||||
| 
 | ||||
|         this._groupStores = {}; | ||||
|         this._selectedTagsRoomIds = []; | ||||
|         this._selectedTagsUserIds = []; | ||||
|         // When the selected tags are changed, initialise a group store if necessary
 | ||||
|         this._filterStoreToken = FilterStore.addListener(() => { | ||||
|             FilterStore.getSelectedTags().forEach((tag) => { | ||||
|                 if (tag[0] !== '+' || this._groupStores[tag]) { | ||||
|                     return; | ||||
|                 } | ||||
|                 this._groupStores[tag] = GroupStoreCache.getGroupStore(tag); | ||||
|                 this._groupStores[tag].registerListener(() => { | ||||
|                     this.updateSelectedTagsEntities(); | ||||
|                 }); | ||||
|             }); | ||||
|             this.updateSelectedTagsEntities(); | ||||
|         }); | ||||
| 
 | ||||
|         this.refreshRoomList(); | ||||
| 
 | ||||
|         // order of the sublists
 | ||||
|  | @ -148,6 +168,11 @@ module.exports = React.createClass({ | |||
|             MatrixClientPeg.get().removeListener("accountData", this.onAccountData); | ||||
|             MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership); | ||||
|         } | ||||
| 
 | ||||
|         if (this._filterStoreToken) { | ||||
|             this._filterStoreToken.remove(); | ||||
|         } | ||||
| 
 | ||||
|         // cancel any pending calls to the rate_limited_funcs
 | ||||
|         this._delayedRefreshRoomList.cancelPendingCall(); | ||||
|     }, | ||||
|  | @ -234,6 +259,41 @@ module.exports = React.createClass({ | |||
|         this.refreshRoomList(); | ||||
|     }, 500), | ||||
| 
 | ||||
|     // Update which rooms and users should appear in RoomList as dictated by selected tags
 | ||||
|     updateSelectedTagsEntities: function() { | ||||
|         if (!this.mounted) return; | ||||
|         this._selectedTagsRoomIds = []; | ||||
|         this._selectedTagsUserIds = []; | ||||
|         FilterStore.getSelectedTags().forEach((tag) => { | ||||
|             this._selectedTagsRoomIds = this._selectedTagsRoomIds.concat( | ||||
|                this._groupStores[tag].getGroupRooms().map((room) => room.roomId), | ||||
|             ); | ||||
|             // TODO: Check if room has been tagged to the group by the user
 | ||||
| 
 | ||||
|             this._selectedTagsUserIds = this._selectedTagsUserIds.concat( | ||||
|                this._groupStores[tag].getGroupMembers().map((member) => member.userId), | ||||
|             ); | ||||
|         }); | ||||
|         this.setState({ | ||||
|             selectedTags: FilterStore.getSelectedTags(), | ||||
|         }, () => { | ||||
|             this.refreshRoomList(); | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     isRoomInSelectedTags: function(room, me, dmRoomMap) { | ||||
|         // No selected tags = every room is visible in the list
 | ||||
|         if (this.state.selectedTags.length === 0) { | ||||
|             return true; | ||||
|         } | ||||
|         if (this._selectedTagsRoomIds.includes(room.roomId)) { | ||||
|             return true; | ||||
|         } | ||||
|         const dmUserId = dmRoomMap.getUserIdForRoomId(room.roomId); | ||||
|         return dmUserId && dmUserId !== me.userId && | ||||
|             this._selectedTagsUserIds.includes(dmUserId); | ||||
|     }, | ||||
| 
 | ||||
|     refreshRoomList: function() { | ||||
|         // TODO: ideally we'd calculate this once at start, and then maintain
 | ||||
|         // any changes to it incrementally, updating the appropriate sublists
 | ||||
|  | @ -253,9 +313,7 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     getRoomLists: function() { | ||||
|         const self = this; | ||||
|         const lists = {}; | ||||
| 
 | ||||
|         lists["im.vector.fake.invite"] = []; | ||||
|         lists["m.favourite"] = []; | ||||
|         lists["im.vector.fake.recent"] = []; | ||||
|  | @ -264,8 +322,7 @@ module.exports = React.createClass({ | |||
|         lists["im.vector.fake.archived"] = []; | ||||
| 
 | ||||
|         const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); | ||||
| 
 | ||||
|         MatrixClientPeg.get().getRooms().forEach(function(room) { | ||||
|         MatrixClientPeg.get().getRooms().forEach((room) => { | ||||
|             const me = room.getMember(MatrixClientPeg.get().credentials.userId); | ||||
|             if (!me) return; | ||||
| 
 | ||||
|  | @ -276,13 +333,18 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|             if (me.membership == "invite") { | ||||
|                 lists["im.vector.fake.invite"].push(room); | ||||
|             } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) { | ||||
|             } else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, this.props.ConferenceHandler)) { | ||||
|                 // skip past this room & don't put it in any lists
 | ||||
|             } else if (me.membership == "join" || me.membership === "ban" || | ||||
|                      (me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) { | ||||
|                 // Used to split rooms via tags
 | ||||
|                 const tagNames = Object.keys(room.tags); | ||||
| 
 | ||||
|                 // Apply TagPanel filtering, derived from FilterStore
 | ||||
|                 if (!this.isRoomInSelectedTags(room, me, dmRoomMap)) { | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (tagNames.length) { | ||||
|                     for (let i = 0; i < tagNames.length; i++) { | ||||
|                         const tagName = tagNames[i]; | ||||
|  | @ -474,6 +536,10 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     _getEmptyContent: function(section) { | ||||
|         if (this.state.selectedTags.length > 0) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget'); | ||||
| 
 | ||||
|         if (this.props.collapsed) { | ||||
|  |  | |||
|  | @ -0,0 +1,65 @@ | |||
| /* | ||||
| Copyright 2017 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 {Store} from 'flux/utils'; | ||||
| import dis from '../dispatcher'; | ||||
| import Analytics from '../Analytics'; | ||||
| 
 | ||||
| const INITIAL_STATE = { | ||||
|     tags: [], | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * A class for storing application state for filtering via TagPanel. | ||||
|  */ | ||||
| class FilterStore extends Store { | ||||
|     constructor() { | ||||
|         super(dis); | ||||
| 
 | ||||
|         // Initialise state
 | ||||
|         this._state = INITIAL_STATE; | ||||
|     } | ||||
| 
 | ||||
|     _setState(newState) { | ||||
|         this._state = Object.assign(this._state, newState); | ||||
|         this.__emitChange(); | ||||
|     } | ||||
| 
 | ||||
|     __onDispatch(payload) { | ||||
|         switch (payload.action) { | ||||
|             case 'select_tag': | ||||
|                 this._setState({ | ||||
|                     tags: [payload.tag], | ||||
|                 }); | ||||
|                 Analytics.trackEvent('FilterStore', 'select_tag'); | ||||
|             break; | ||||
|             case 'deselect_tags': | ||||
|                 this._setState({ | ||||
|                     tags: [], | ||||
|                 }); | ||||
|                 Analytics.trackEvent('FilterStore', 'deselect_tags'); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     getSelectedTags() { | ||||
|         return this._state.tags; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| if (global.singletonFilterStore === undefined) { | ||||
|     global.singletonFilterStore = new FilterStore(); | ||||
| } | ||||
| export default global.singletonFilterStore; | ||||
|  | @ -28,7 +28,6 @@ class GroupStoreCache { | |||
|             // referencing it.
 | ||||
|             this.groupStore = new GroupStore(groupId); | ||||
|         } | ||||
|         this.groupStore._fetchSummary(); | ||||
|         return this.groupStore; | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Luke Barnard
						Luke Barnard