Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
fb92be765c
6 changed files with 317 additions and 6 deletions
|
@ -213,6 +213,7 @@ export default React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||||
const RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
|
@ -334,6 +335,7 @@ export default React.createClass({
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper'>
|
||||||
{ topBar }
|
{ topBar }
|
||||||
<div className={bodyClasses}>
|
<div className={bodyClasses}>
|
||||||
|
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
selectedRoom={this.props.currentRoomId}
|
selectedRoom={this.props.currentRoomId}
|
||||||
collapsed={this.props.collapseLhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
|
|
173
src/components/structures/TagPanel.js
Normal file
173
src/components/structures/TagPanel.js
Normal file
|
@ -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.unmounted = false;
|
||||||
|
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
|
||||||
|
this._filterStoreToken = FilterStore.addListener(() => {
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedTags: FilterStore.getSelectedTags(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._fetchJoinedRooms();
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
if (this._filterStoreToken) {
|
||||||
|
this._filterStoreToken.remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onGroupMyMembership() {
|
||||||
|
if (this.unmounted) 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');
|
const Rooms = require('../../../Rooms');
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
const Receipt = require('../../../utils/Receipt');
|
const Receipt = require('../../../utils/Receipt');
|
||||||
|
import FilterStore from '../../../stores/FilterStore';
|
||||||
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
|
|
||||||
const HIDE_CONFERENCE_CHANS = true;
|
const HIDE_CONFERENCE_CHANS = true;
|
||||||
|
|
||||||
|
@ -61,6 +63,7 @@ module.exports = React.createClass({
|
||||||
totalRoomCount: null,
|
totalRoomCount: null,
|
||||||
lists: {},
|
lists: {},
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
|
selectedTags: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -80,6 +83,23 @@ module.exports = React.createClass({
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
cli.on("Group.myMembership", this._onGroupMyMembership);
|
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();
|
this.refreshRoomList();
|
||||||
|
|
||||||
// order of the sublists
|
// order of the sublists
|
||||||
|
@ -148,6 +168,11 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._filterStoreToken) {
|
||||||
|
this._filterStoreToken.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// cancel any pending calls to the rate_limited_funcs
|
// cancel any pending calls to the rate_limited_funcs
|
||||||
this._delayedRefreshRoomList.cancelPendingCall();
|
this._delayedRefreshRoomList.cancelPendingCall();
|
||||||
},
|
},
|
||||||
|
@ -234,6 +259,41 @@ module.exports = React.createClass({
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
}, 500),
|
}, 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() {
|
refreshRoomList: function() {
|
||||||
// TODO: ideally we'd calculate this once at start, and then maintain
|
// TODO: ideally we'd calculate this once at start, and then maintain
|
||||||
// any changes to it incrementally, updating the appropriate sublists
|
// any changes to it incrementally, updating the appropriate sublists
|
||||||
|
@ -253,9 +313,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
const self = this;
|
|
||||||
const lists = {};
|
const lists = {};
|
||||||
|
|
||||||
lists["im.vector.fake.invite"] = [];
|
lists["im.vector.fake.invite"] = [];
|
||||||
lists["m.favourite"] = [];
|
lists["m.favourite"] = [];
|
||||||
lists["im.vector.fake.recent"] = [];
|
lists["im.vector.fake.recent"] = [];
|
||||||
|
@ -264,8 +322,7 @@ module.exports = React.createClass({
|
||||||
lists["im.vector.fake.archived"] = [];
|
lists["im.vector.fake.archived"] = [];
|
||||||
|
|
||||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||||
|
MatrixClientPeg.get().getRooms().forEach((room) => {
|
||||||
MatrixClientPeg.get().getRooms().forEach(function(room) {
|
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
|
@ -276,13 +333,18 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
if (me.membership == "invite") {
|
if (me.membership == "invite") {
|
||||||
lists["im.vector.fake.invite"].push(room);
|
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
|
// skip past this room & don't put it in any lists
|
||||||
} else if (me.membership == "join" || me.membership === "ban" ||
|
} else if (me.membership == "join" || me.membership === "ban" ||
|
||||||
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
|
||||||
// Used to split rooms via tags
|
// Used to split rooms via tags
|
||||||
const tagNames = Object.keys(room.tags);
|
const tagNames = Object.keys(room.tags);
|
||||||
|
|
||||||
|
// Apply TagPanel filtering, derived from FilterStore
|
||||||
|
if (!this.isRoomInSelectedTags(room, me, dmRoomMap)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (tagNames.length) {
|
if (tagNames.length) {
|
||||||
for (let i = 0; i < tagNames.length; i++) {
|
for (let i = 0; i < tagNames.length; i++) {
|
||||||
const tagName = tagNames[i];
|
const tagName = tagNames[i];
|
||||||
|
@ -474,6 +536,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getEmptyContent: function(section) {
|
_getEmptyContent: function(section) {
|
||||||
|
if (this.state.selectedTags.length > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
|
||||||
|
|
||||||
if (this.props.collapsed) {
|
if (this.props.collapsed) {
|
||||||
|
|
|
@ -88,6 +88,12 @@ export const SETTINGS = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_tag_panel": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Tag Panel"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"MessageComposerInput.dontSuggestEmoji": {
|
"MessageComposerInput.dontSuggestEmoji": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Disable Emoji suggestions while typing'),
|
displayName: _td('Disable Emoji suggestions while typing'),
|
||||||
|
|
65
src/stores/FilterStore.js
Normal file
65
src/stores/FilterStore.js
Normal file
|
@ -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.
|
// referencing it.
|
||||||
this.groupStore = new GroupStore(groupId);
|
this.groupStore = new GroupStore(groupId);
|
||||||
}
|
}
|
||||||
this.groupStore._fetchSummary();
|
|
||||||
return this.groupStore;
|
return this.groupStore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue