Merge branch 'develop' into luke/perf-tag-panel-selected

This commit is contained in:
lukebarnard 2018-01-05 10:23:20 +00:00
commit 7606e60188
9 changed files with 99 additions and 151 deletions

View file

@ -68,3 +68,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) {
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
}
export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
if (isMac) {
return ev.metaKey && !ev.altKey && !ev.ctrlKey;
} else {
return ev.ctrlKey && !ev.altKey && !ev.metaKey;
}
}

View file

@ -333,7 +333,6 @@ const LoggedInView = React.createClass({
<div className={bodyClasses}>
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
<LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import FilterStore from '../../stores/FilterStore';
import TagOrderStore from '../../stores/TagOrderStore';
import GroupActions from '../../actions/GroupActions';
@ -44,20 +43,13 @@ const TagPanel = React.createClass({
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._tagOrderStoreToken = TagOrderStore.addListener(() => {
if (this.unmounted) {
return;
}
this.setState({
orderedTags: TagOrderStore.getOrderedTags() || [],
selectedTags: TagOrderStore.getSelectedTags(),
});
});
// This could be done by anything with a matrix client

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import FlairStore from '../../../stores/FlairStore';
@ -76,7 +76,7 @@ export default React.createClass({
dis.dispatch({
action: 'select_tag',
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
},

View file

@ -41,7 +41,6 @@ import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = withMatrixClient(React.createClass({
displayName: 'MemberInfo',
@ -713,6 +712,10 @@ module.exports = withMatrixClient(React.createClass({
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
const dmRoomMap = new DMRoomMap(this.props.matrixClient);
// dmRooms will not include dmRooms that we have been invited into but did not join.
// Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room.
// XXX: we potentially want DMs we have been invited to, to also show up here :L
// especially as logic below concerns specially if we haven't joined but have been invited
const dmRooms = dmRoomMap.getDMRoomsForUserId(this.props.member.userId);
const RoomTile = sdk.getComponent("rooms.RoomTile");
@ -722,10 +725,15 @@ module.exports = withMatrixClient(React.createClass({
const room = this.props.matrixClient.getRoom(roomId);
if (room) {
const me = room.getMember(this.props.matrixClient.credentials.userId);
const highlight = (
room.getUnreadNotificationCount('highlight') > 0 ||
me.membership === "invite"
);
// not a DM room if we have are not joined
if (!me.membership || me.membership !== 'join') continue;
// not a DM room if they are not joined
const them = this.props.member;
if (!them.membership || them.membership !== 'join') continue;
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === 'invite';
tiles.push(
<RoomTile key={room.roomId} room={room}
collapsed={false}

View file

@ -512,7 +512,8 @@ export default class MessageComposerInput extends React.Component {
// composer. For some reason the editor won't scroll automatically if we paste
// blocks of text in or insert newlines.
if (textContent.slice(selection.start).indexOf("\n") === -1) {
this.refs.editor.refs.editor.scrollTop = this.refs.editor.refs.editor.scrollHeight;
let editorRoot = this.refs.editor.refs.editor.parentNode.parentNode;
editorRoot.scrollTop = editorRoot.scrollHeight;
}
});
}

View file

@ -28,7 +28,7 @@ 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 TagOrderStore from '../../../stores/TagOrderStore';
import GroupStoreCache from '../../../stores/GroupStoreCache';
const HIDE_CONFERENCE_CHANS = true;
@ -96,8 +96,8 @@ module.exports = React.createClass({
// By default, set to `null` meaning "all rooms visible"
this._visibleRooms = null;
// When the selected tags are changed, initialise a group store if necessary
this._filterStoreToken = FilterStore.addListener(() => {
FilterStore.getSelectedTags().forEach((tag) => {
this._tagStoreToken = TagOrderStore.addListener(() => {
TagOrderStore.getSelectedTags().forEach((tag) => {
if (tag[0] !== '+' || this._groupStores[tag]) {
return;
}
@ -183,8 +183,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
}
if (this._filterStoreToken) {
this._filterStoreToken.remove();
if (this._tagStoreToken) {
this._tagStoreToken.remove();
}
if (this._groupStoreTokens.length > 0) {
@ -298,12 +298,11 @@ module.exports = React.createClass({
// Update which rooms and users should appear according to which tags are selected
updateVisibleRooms: function() {
const selectedTags = FilterStore.getSelectedTags();
let visibleGroupRooms = [];
const selectedTags = TagOrderStore.getSelectedTags();
const visibleGroupRooms = [];
selectedTags.forEach((tag) => {
visibleGroupRooms = visibleGroupRooms.concat(
this._visibleRoomsForGroup[tag] || [],
(this._visibleRoomsForGroup[tag] || []).forEach(
(roomId) => visibleGroupRooms.push(roomId),
);
});
@ -378,7 +377,6 @@ module.exports = React.createClass({
(me.membership === "leave" && me.events.member.getSender() !== me.events.member.getStateKey())) {
// Used to split rooms via tags
const tagNames = Object.keys(room.tags);
if (tagNames.length) {
for (let i = 0; i < tagNames.length; i++) {
const tagName = tagNames[i];
@ -672,7 +670,6 @@ module.exports = React.createClass({
editable={false}
order="recent"
isInvite={true}
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -686,7 +683,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent('m.favourite')}
editable={true}
order="manual"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -700,7 +696,6 @@ module.exports = React.createClass({
headerItems={this._getHeaderItems('im.vector.fake.direct')}
editable={true}
order="recent"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
alwaysShowHeader={true}
@ -714,7 +709,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
headerItems={this._getHeaderItems('im.vector.fake.recent')}
order="recent"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -730,7 +724,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent(tagName)}
editable={true}
order="manual"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -745,7 +738,6 @@ module.exports = React.createClass({
emptyContent={this._getEmptyContent('m.lowpriority')}
editable={true}
order="recent"
selectedRoom={self.props.selectedRoom}
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
@ -756,7 +748,6 @@ module.exports = React.createClass({
label={_t('Historical')}
editable={false}
order="recent"
selectedRoom={self.props.selectedRoom}
collapsed={self.props.collapsed}
alwaysShowHeader={true}
startAsHidden={true}

View file

@ -1,115 +0,0 @@
/*
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 = {
allTags: [],
selectedTags: [],
// Last selected tag when shift was not being pressed
anchorTag: null,
};
/**
* 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 'all_tags' :
this._setState({
allTags: payload.tags,
});
break;
case 'select_tag': {
let newTags = [];
// Shift-click semantics
if (payload.shiftKey) {
// Select range of tags
let start = this._state.allTags.indexOf(this._state.anchorTag);
let end = this._state.allTags.indexOf(payload.tag);
if (start === -1) {
start = end;
}
if (start > end) {
const temp = start;
start = end;
end = temp;
}
newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : [];
newTags = [...new Set(
this._state.allTags.slice(start, end + 1).concat(newTags),
)];
} else {
if (payload.ctrlOrCmdKey) {
// Toggle individual tag
if (this._state.selectedTags.includes(payload.tag)) {
newTags = this._state.selectedTags.filter((t) => t !== payload.tag);
} else {
newTags = [...this._state.selectedTags, payload.tag];
}
} else {
// Select individual tag
newTags = [payload.tag];
}
// Only set the anchor tag if the tag was previously unselected, otherwise
// the next range starts with an unselected tag.
if (!this._state.selectedTags.includes(payload.tag)) {
this._setState({
anchorTag: payload.tag,
});
}
}
this._setState({
selectedTags: newTags,
});
Analytics.trackEvent('FilterStore', 'select_tag');
}
break;
case 'deselect_tags':
this._setState({
selectedTags: [],
});
Analytics.trackEvent('FilterStore', 'deselect_tags');
break;
}
}
getSelectedTags() {
return this._state.selectedTags;
}
}
if (global.singletonFilterStore === undefined) {
global.singletonFilterStore = new FilterStore();
}
export default global.singletonFilterStore;

View file

@ -15,12 +15,17 @@ limitations under the License.
*/
import {Store} from 'flux/utils';
import dis from '../dispatcher';
import Analytics from '../Analytics';
const INITIAL_STATE = {
orderedTags: null,
orderedTagsAccountData: null,
hasSynced: false,
joinedGroupIds: null,
selectedTags: [],
// Last selected tag when shift was not being pressed
anchorTag: null,
};
/**
@ -93,6 +98,60 @@ class TagOrderStore extends Store {
this._setState({orderedTags});
break;
}
case 'select_tag': {
let newTags = [];
// Shift-click semantics
if (payload.shiftKey) {
// Select range of tags
let start = this._state.orderedTags.indexOf(this._state.anchorTag);
let end = this._state.orderedTags.indexOf(payload.tag);
if (start === -1) {
start = end;
}
if (start > end) {
const temp = start;
start = end;
end = temp;
}
newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : [];
newTags = [...new Set(
this._state.orderedTags.slice(start, end + 1).concat(newTags),
)];
} else {
if (payload.ctrlOrCmdKey) {
// Toggle individual tag
if (this._state.selectedTags.includes(payload.tag)) {
newTags = this._state.selectedTags.filter((t) => t !== payload.tag);
} else {
newTags = [...this._state.selectedTags, payload.tag];
}
} else {
// Select individual tag
newTags = [payload.tag];
}
// Only set the anchor tag if the tag was previously unselected, otherwise
// the next range starts with an unselected tag.
if (!this._state.selectedTags.includes(payload.tag)) {
this._setState({
anchorTag: payload.tag,
});
}
}
this._setState({
selectedTags: newTags,
});
Analytics.trackEvent('FilterStore', 'select_tag');
}
break;
case 'deselect_tags':
this._setState({
selectedTags: [],
});
Analytics.trackEvent('FilterStore', 'deselect_tags');
break;
case 'on_logged_out': {
// Reset state without pushing an update to the view, which generally assumes that
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
@ -129,6 +188,10 @@ class TagOrderStore extends Store {
getOrderedTags() {
return this._state.orderedTags;
}
getSelectedTags() {
return this._state.selectedTags;
}
}
if (global.singletonTagOrderStore === undefined) {