Merge branch 'develop' into luke/feature-tag-panel-tile-context-menu
This commit is contained in:
commit
a34fea8af8
8 changed files with 576 additions and 315 deletions
|
@ -62,6 +62,14 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||||
|
return { action: 'MatrixActions.Room.tags', room };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) {
|
||||||
|
return { action: 'MatrixActions.RoomMember.membership', member };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This object is responsible for dispatching actions when certain events are emitted by
|
* This object is responsible for dispatching actions when certain events are emitted by
|
||||||
* the given MatrixClient.
|
* the given MatrixClient.
|
||||||
|
@ -78,6 +86,8 @@ export default {
|
||||||
start(matrixClient) {
|
start(matrixClient) {
|
||||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||||
|
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||||
|
this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createRoomMembershipAction);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,7 +101,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
||||||
const listener = (...args) => {
|
const listener = (...args) => {
|
||||||
dis.dispatch(actionCreator(matrixClient, ...args));
|
dis.dispatch(actionCreator(matrixClient, ...args), true);
|
||||||
};
|
};
|
||||||
matrixClient.on(eventName, listener);
|
matrixClient.on(eventName, listener);
|
||||||
this._matrixClientListenersStop.push(() => {
|
this._matrixClientListenersStop.push(() => {
|
||||||
|
|
146
src/actions/RoomListActions.js
Normal file
146
src/actions/RoomListActions.js
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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 { asyncAction } from './actionCreators';
|
||||||
|
import RoomListStore from '../stores/RoomListStore';
|
||||||
|
|
||||||
|
import Modal from '../Modal';
|
||||||
|
import Rooms from '../Rooms';
|
||||||
|
import { _t } from '../languageHandler';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
|
const RoomListActions = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an action thunk that will do an asynchronous request to
|
||||||
|
* tag room.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||||
|
* account data on.
|
||||||
|
* @param {Room} room the room to tag.
|
||||||
|
* @param {string} oldTag the tag to remove (unless oldTag ==== newTag)
|
||||||
|
* @param {string} newTag the tag with which to tag the room.
|
||||||
|
* @param {?number} oldIndex the previous position of the room in the
|
||||||
|
* list of rooms.
|
||||||
|
* @param {?number} newIndex the new position of the room in the list
|
||||||
|
* of rooms.
|
||||||
|
* @returns {function} an action thunk.
|
||||||
|
* @see asyncAction
|
||||||
|
*/
|
||||||
|
RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) {
|
||||||
|
let metaData = null;
|
||||||
|
|
||||||
|
// Is the tag ordered manually?
|
||||||
|
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||||
|
const lists = RoomListStore.getRoomLists();
|
||||||
|
const newList = [...lists[newTag]];
|
||||||
|
|
||||||
|
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||||
|
|
||||||
|
// If the room was moved "down" (increasing index) in the same list we
|
||||||
|
// need to use the orders of the tiles with indices shifted by +1
|
||||||
|
const offset = (
|
||||||
|
newTag === oldTag && oldIndex < newIndex
|
||||||
|
) ? 1 : 0;
|
||||||
|
|
||||||
|
const indexBefore = offset + newIndex - 1;
|
||||||
|
const indexAfter = offset + newIndex;
|
||||||
|
|
||||||
|
const prevOrder = indexBefore <= 0 ?
|
||||||
|
0 : newList[indexBefore].tags[newTag].order;
|
||||||
|
const nextOrder = indexAfter >= newList.length ?
|
||||||
|
1 : newList[indexAfter].tags[newTag].order;
|
||||||
|
|
||||||
|
metaData = {
|
||||||
|
order: (prevOrder + nextOrder) / 2.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return asyncAction('RoomListActions.tagRoom', () => {
|
||||||
|
const promises = [];
|
||||||
|
const roomId = room.roomId;
|
||||||
|
|
||||||
|
// Evil hack to get DMs behaving
|
||||||
|
if ((oldTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||||
|
(oldTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||||
|
) {
|
||||||
|
return Rooms.guessAndSetDMRoom(
|
||||||
|
room, newTag === 'im.vector.fake.direct',
|
||||||
|
).catch((err) => {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to set direct chat tag " + err);
|
||||||
|
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||||
|
title: _t('Failed to set direct chat tag'),
|
||||||
|
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChangedSubLists = oldTag !== newTag;
|
||||||
|
|
||||||
|
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||||
|
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||||
|
//
|
||||||
|
// if we moved lists, remove the old tag
|
||||||
|
if (oldTag && oldTag !== 'im.vector.fake.direct' &&
|
||||||
|
hasChangedSubLists
|
||||||
|
) {
|
||||||
|
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||||
|
roomId, oldTag,
|
||||||
|
).catch(function(err) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to remove tag " + oldTag + " from room: " + err);
|
||||||
|
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||||
|
title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}),
|
||||||
|
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(promiseToDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we moved lists or the ordering changed, add the new tag
|
||||||
|
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||||
|
(hasChangedSubLists || metaData)
|
||||||
|
) {
|
||||||
|
// metaData is the body of the PUT to set the tag, so it must
|
||||||
|
// at least be an empty object.
|
||||||
|
metaData = metaData || {};
|
||||||
|
|
||||||
|
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||||
|
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||||
|
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||||
|
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||||
|
});
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(promiseToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
}, () => {
|
||||||
|
// For an optimistic update
|
||||||
|
return {
|
||||||
|
room, oldTag, newTag, metaData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoomListActions;
|
|
@ -31,6 +31,15 @@ limitations under the License.
|
||||||
* `${id}.pending` and either
|
* `${id}.pending` and either
|
||||||
* `${id}.success` or
|
* `${id}.success` or
|
||||||
* `${id}.failure`.
|
* `${id}.failure`.
|
||||||
|
*
|
||||||
|
* The shape of each are:
|
||||||
|
* { action: '${id}.pending', request }
|
||||||
|
* { action: '${id}.success', result }
|
||||||
|
* { action: '${id}.failure', err }
|
||||||
|
*
|
||||||
|
* where `request` is returned by `pendingFn` and
|
||||||
|
* result is the result of the promise returned by
|
||||||
|
* `fn`.
|
||||||
*/
|
*/
|
||||||
export function asyncAction(id, fn, pendingFn) {
|
export function asyncAction(id, fn, pendingFn) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
|
|
|
@ -208,7 +208,6 @@ const LoggedInView = 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');
|
||||||
|
@ -330,7 +329,6 @@ const LoggedInView = 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
|
||||||
collapsed={this.props.collapseLhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
disabled={this.props.leftDisabled}
|
disabled={this.props.leftDisabled}
|
||||||
|
|
|
@ -20,12 +20,11 @@ import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import TagOrderStore from '../../stores/TagOrderStore';
|
import TagOrderStore from '../../stores/TagOrderStore';
|
||||||
|
|
||||||
import GroupActions from '../../actions/GroupActions';
|
import GroupActions from '../../actions/GroupActions';
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
|
||||||
|
|
||||||
import sdk from '../../index';
|
import sdk from '../../index';
|
||||||
import dis from '../../dispatcher';
|
import dis from '../../dispatcher';
|
||||||
|
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { Droppable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
const TagPanel = React.createClass({
|
const TagPanel = React.createClass({
|
||||||
displayName: 'TagPanel',
|
displayName: 'TagPanel',
|
||||||
|
@ -94,25 +93,8 @@ const TagPanel = React.createClass({
|
||||||
dis.dispatch({action: 'view_create_group'});
|
dis.dispatch({action: 'view_create_group'});
|
||||||
},
|
},
|
||||||
|
|
||||||
onTagTileEndDrag(result) {
|
|
||||||
// Dragged to an invalid destination, not onto a droppable
|
|
||||||
if (!result.destination) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch synchronously so that the TagPanel receives an
|
|
||||||
// optimistic update from TagOrderStore before the previous
|
|
||||||
// state is shown.
|
|
||||||
dis.dispatch(TagOrderActions.moveTag(
|
|
||||||
this.context.matrixClient,
|
|
||||||
result.draggableId,
|
|
||||||
result.destination.index,
|
|
||||||
), true);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
|
||||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||||
|
|
||||||
const tags = this.state.orderedTags.map((tag, index) => {
|
const tags = this.state.orderedTags.map((tag, index) => {
|
||||||
|
@ -124,8 +106,10 @@ const TagPanel = React.createClass({
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
return <div className="mx_TagPanel">
|
return <div className="mx_TagPanel">
|
||||||
<DragDropContext onDragEnd={this.onTagTileEndDrag}>
|
<Droppable
|
||||||
<Droppable droppableId="tag-panel-droppable">
|
droppableId="tag-panel-droppable"
|
||||||
|
type="draggable-TagTile"
|
||||||
|
>
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className="mx_TagPanel_tagTileContainer"
|
className="mx_TagPanel_tagTileContainer"
|
||||||
|
@ -141,10 +125,9 @@ const TagPanel = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
) }
|
) }
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
<div className="mx_TagPanel_createGroupButton">
|
||||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
<GroupsButton tooltip={true} />
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
</div>
|
||||||
</AccessibleButton>
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default function DNDTagTile(props) {
|
||||||
key={props.tag}
|
key={props.tag}
|
||||||
draggableId={props.tag}
|
draggableId={props.tag}
|
||||||
index={props.index}
|
index={props.index}
|
||||||
|
type="draggable-TagTile"
|
||||||
>
|
>
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
const React = require("react");
|
const React = require("react");
|
||||||
const ReactDOM = require("react-dom");
|
const ReactDOM = require("react-dom");
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
@ -27,14 +26,13 @@ const CallHandler = require('../../../CallHandler');
|
||||||
const dis = require("../../../dispatcher");
|
const dis = require("../../../dispatcher");
|
||||||
const sdk = require('../../../index');
|
const sdk = require('../../../index');
|
||||||
const rate_limited_func = require('../../../ratelimitedfunc');
|
const rate_limited_func = require('../../../ratelimitedfunc');
|
||||||
const Rooms = require('../../../Rooms');
|
import * as Rooms from '../../../Rooms';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
const Receipt = require('../../../utils/Receipt');
|
const Receipt = require('../../../utils/Receipt');
|
||||||
import TagOrderStore from '../../../stores/TagOrderStore';
|
import TagOrderStore from '../../../stores/TagOrderStore';
|
||||||
|
import RoomListStore from '../../../stores/RoomListStore';
|
||||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||||
|
|
||||||
import Modal from '../../../Modal';
|
|
||||||
|
|
||||||
const HIDE_CONFERENCE_CHANS = true;
|
const HIDE_CONFERENCE_CHANS = true;
|
||||||
|
|
||||||
function phraseForSection(section) {
|
function phraseForSection(section) {
|
||||||
|
@ -80,7 +78,6 @@ module.exports = React.createClass({
|
||||||
cli.on("deleteRoom", this.onDeleteRoom);
|
cli.on("deleteRoom", this.onDeleteRoom);
|
||||||
cli.on("Room.timeline", this.onRoomTimeline);
|
cli.on("Room.timeline", this.onRoomTimeline);
|
||||||
cli.on("Room.name", this.onRoomName);
|
cli.on("Room.name", this.onRoomName);
|
||||||
cli.on("Room.tags", this.onRoomTags);
|
|
||||||
cli.on("Room.receipt", this.onRoomReceipt);
|
cli.on("Room.receipt", this.onRoomReceipt);
|
||||||
cli.on("RoomState.events", this.onRoomStateEvents);
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
cli.on("RoomMember.name", this.onRoomMemberName);
|
cli.on("RoomMember.name", this.onRoomMemberName);
|
||||||
|
@ -118,6 +115,10 @@ module.exports = React.createClass({
|
||||||
this.updateVisibleRooms();
|
this.updateVisibleRooms();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._roomListStoreToken = RoomListStore.addListener(() => {
|
||||||
|
this._delayedRefreshRoomList();
|
||||||
|
});
|
||||||
|
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
|
|
||||||
// order of the sublists
|
// order of the sublists
|
||||||
|
@ -178,7 +179,6 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
|
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||||
MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags);
|
|
||||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
|
||||||
|
@ -251,10 +251,6 @@ module.exports = React.createClass({
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTags: function(event, room) {
|
|
||||||
this._delayedRefreshRoomList();
|
|
||||||
},
|
|
||||||
|
|
||||||
onRoomStateEvents: function(ev, state) {
|
onRoomStateEvents: function(ev, state) {
|
||||||
this._delayedRefreshRoomList();
|
this._delayedRefreshRoomList();
|
||||||
},
|
},
|
||||||
|
@ -278,106 +274,6 @@ module.exports = React.createClass({
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomTileEndDrag: function(result) {
|
|
||||||
if (!result.destination) return;
|
|
||||||
|
|
||||||
let newTag = result.destination.droppableId.split('_')[1];
|
|
||||||
let prevTag = result.source.droppableId.split('_')[1];
|
|
||||||
if (newTag === 'undefined') newTag = undefined;
|
|
||||||
if (prevTag === 'undefined') prevTag = undefined;
|
|
||||||
|
|
||||||
const roomId = result.draggableId.split('_')[1];
|
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
|
||||||
|
|
||||||
const newIndex = result.destination.index;
|
|
||||||
|
|
||||||
// Evil hack to get DMs behaving
|
|
||||||
if ((prevTag === undefined && newTag === 'im.vector.fake.direct') ||
|
|
||||||
(prevTag === 'im.vector.fake.direct' && newTag === undefined)
|
|
||||||
) {
|
|
||||||
Rooms.guessAndSetDMRoom(
|
|
||||||
room, newTag === 'im.vector.fake.direct',
|
|
||||||
).catch((err) => {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to set direct chat tag " + err);
|
|
||||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to set direct chat tag'),
|
|
||||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChangedSubLists = result.source.droppableId !== result.destination.droppableId;
|
|
||||||
|
|
||||||
let newOrder = null;
|
|
||||||
|
|
||||||
// Is the tag ordered manually?
|
|
||||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
|
||||||
const newList = this.state.lists[newTag];
|
|
||||||
|
|
||||||
// If the room was moved "down" (increasing index) in the same list we
|
|
||||||
// need to use the orders of the tiles with indices shifted by +1
|
|
||||||
const offset = (
|
|
||||||
newTag === prevTag && result.source.index < result.destination.index
|
|
||||||
) ? 1 : 0;
|
|
||||||
|
|
||||||
const indexBefore = offset + newIndex - 1;
|
|
||||||
const indexAfter = offset + newIndex;
|
|
||||||
|
|
||||||
const prevOrder = indexBefore < 0 ?
|
|
||||||
0 : newList[indexBefore].tags[newTag].order;
|
|
||||||
const nextOrder = indexAfter >= newList.length ?
|
|
||||||
1 : newList[indexAfter].tags[newTag].order;
|
|
||||||
|
|
||||||
newOrder = {
|
|
||||||
order: (prevOrder + nextOrder) / 2.0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
|
||||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
|
||||||
//
|
|
||||||
// if we moved lists, remove the old tag
|
|
||||||
if (prevTag && prevTag !== 'im.vector.fake.direct' &&
|
|
||||||
hasChangedSubLists
|
|
||||||
) {
|
|
||||||
// Optimistic update of what will happen to the room tags
|
|
||||||
delete room.tags[prevTag];
|
|
||||||
|
|
||||||
MatrixClientPeg.get().deleteRoomTag(roomId, prevTag).catch(function(err) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to remove tag " + prevTag + " from room: " + err);
|
|
||||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: prevTag}),
|
|
||||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we moved lists or the ordering changed, add the new tag
|
|
||||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
|
||||||
(hasChangedSubLists || newOrder)
|
|
||||||
) {
|
|
||||||
// Optimistic update of what will happen to the room tags
|
|
||||||
room.tags[newTag] = newOrder;
|
|
||||||
|
|
||||||
MatrixClientPeg.get().setRoomTag(roomId, newTag, newOrder).catch(function(err) {
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
||||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
|
||||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
|
||||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
|
||||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh to display the optimistic updates - this needs to be done in the
|
|
||||||
// same tick as the drag finishing otherwise the room will pop back to its
|
|
||||||
// previous position - hence no delayed refresh
|
|
||||||
this.refreshRoomList();
|
|
||||||
},
|
|
||||||
|
|
||||||
_delayedRefreshRoomList: new rate_limited_func(function() {
|
_delayedRefreshRoomList: new rate_limited_func(function() {
|
||||||
this.refreshRoomList();
|
this.refreshRoomList();
|
||||||
}, 500),
|
}, 500),
|
||||||
|
@ -441,7 +337,7 @@ module.exports = React.createClass({
|
||||||
totalRooms += l.length;
|
totalRooms += l.length;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
lists: this.getRoomLists(),
|
lists,
|
||||||
totalRoomCount: totalRooms,
|
totalRoomCount: totalRooms,
|
||||||
// Do this here so as to not render every time the selected tags
|
// Do this here so as to not render every time the selected tags
|
||||||
// themselves change.
|
// themselves change.
|
||||||
|
@ -452,70 +348,34 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomLists: function() {
|
getRoomLists: function() {
|
||||||
const lists = {};
|
const lists = RoomListStore.getRoomLists();
|
||||||
lists["im.vector.fake.invite"] = [];
|
|
||||||
lists["m.favourite"] = [];
|
|
||||||
lists["im.vector.fake.recent"] = [];
|
|
||||||
lists["im.vector.fake.direct"] = [];
|
|
||||||
lists["m.lowpriority"] = [];
|
|
||||||
lists["im.vector.fake.archived"] = [];
|
|
||||||
|
|
||||||
const dmRoomMap = DMRoomMap.shared();
|
const filteredLists = {};
|
||||||
|
|
||||||
this._visibleRooms.forEach((room, index) => {
|
const isRoomVisible = {
|
||||||
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
|
// $roomId: true,
|
||||||
if (!me) return;
|
};
|
||||||
|
|
||||||
// console.log("room = " + room.name + ", me.membership = " + me.membership +
|
this._visibleRooms.forEach((r) => {
|
||||||
// ", sender = " + me.events.member.getSender() +
|
isRoomVisible[r.roomId] = true;
|
||||||
// ", target = " + me.events.member.getStateKey() +
|
|
||||||
// ", prevMembership = " + me.events.member.getPrevContent().membership);
|
|
||||||
|
|
||||||
if (me.membership == "invite") {
|
|
||||||
lists["im.vector.fake.invite"].push(room);
|
|
||||||
} 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);
|
|
||||||
if (tagNames.length) {
|
|
||||||
for (let i = 0; i < tagNames.length; i++) {
|
|
||||||
const tagName = tagNames[i];
|
|
||||||
lists[tagName] = lists[tagName] || [];
|
|
||||||
lists[tagName].push(room);
|
|
||||||
}
|
|
||||||
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
|
||||||
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
|
||||||
lists["im.vector.fake.direct"].push(room);
|
|
||||||
} else {
|
|
||||||
lists["im.vector.fake.recent"].push(room);
|
|
||||||
}
|
|
||||||
} else if (me.membership === "leave") {
|
|
||||||
lists["im.vector.fake.archived"].push(room);
|
|
||||||
} else {
|
|
||||||
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
|
Object.keys(lists).forEach((tagName) => {
|
||||||
|
filteredLists[tagName] = lists[tagName].filter((taggedRoom) => {
|
||||||
|
// Somewhat impossible, but guard against it anyway
|
||||||
|
if (!taggedRoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
|
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
|
return Boolean(isRoomVisible[taggedRoom.roomId]);
|
||||||
/*
|
});
|
||||||
this.listOrder = [
|
});
|
||||||
"im.vector.fake.invite",
|
|
||||||
"m.favourite",
|
|
||||||
"im.vector.fake.recent",
|
|
||||||
"im.vector.fake.direct",
|
|
||||||
Object.keys(otherTagNames).filter(tagName=>{
|
|
||||||
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
|
|
||||||
}).sort(),
|
|
||||||
"m.lowpriority",
|
|
||||||
"im.vector.fake.archived"
|
|
||||||
];
|
|
||||||
*/
|
|
||||||
|
|
||||||
return lists;
|
return filteredLists;
|
||||||
},
|
},
|
||||||
|
|
||||||
_getScrollNode: function() {
|
_getScrollNode: function() {
|
||||||
|
@ -752,7 +612,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={this.onRoomTileEndDrag}>
|
|
||||||
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
<GeminiScrollbar className="mx_RoomList_scrollbar"
|
||||||
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
|
||||||
<div className="mx_RoomList">
|
<div className="mx_RoomList">
|
||||||
|
@ -861,7 +720,6 @@ module.exports = React.createClass({
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />
|
onShowMoreRooms={self.onShowMoreRooms} />
|
||||||
</div>
|
</div>
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
256
src/stores/RoomListStore.js
Normal file
256
src/stores/RoomListStore.js
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
/*
|
||||||
|
Copyright 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.
|
||||||
|
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 DMRoomMap from '../utils/DMRoomMap';
|
||||||
|
import Unread from '../Unread';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for storing application state for categorising rooms in
|
||||||
|
* the RoomList.
|
||||||
|
*/
|
||||||
|
class RoomListStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super(dis);
|
||||||
|
|
||||||
|
this._init();
|
||||||
|
this._getManualComparator = this._getManualComparator.bind(this);
|
||||||
|
this._recentsComparator = this._recentsComparator.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_init() {
|
||||||
|
// Initialise state
|
||||||
|
this._state = {
|
||||||
|
lists: {
|
||||||
|
"im.vector.fake.invite": [],
|
||||||
|
"m.favourite": [],
|
||||||
|
"im.vector.fake.recent": [],
|
||||||
|
"im.vector.fake.direct": [],
|
||||||
|
"m.lowpriority": [],
|
||||||
|
"im.vector.fake.archived": [],
|
||||||
|
},
|
||||||
|
ready: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setState(newState) {
|
||||||
|
this._state = Object.assign(this._state, newState);
|
||||||
|
this.__emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
__onDispatch(payload) {
|
||||||
|
switch (payload.action) {
|
||||||
|
// Initialise state after initial sync
|
||||||
|
case 'MatrixActions.sync': {
|
||||||
|
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._matrixClient = payload.matrixClient;
|
||||||
|
this._generateRoomLists();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'MatrixActions.Room.tags': {
|
||||||
|
if (!this._state.ready) break;
|
||||||
|
this._generateRoomLists();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'MatrixActions.accountData': {
|
||||||
|
if (payload.event_type !== 'm.direct') break;
|
||||||
|
this._generateRoomLists();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'MatrixActions.RoomMember.membership': {
|
||||||
|
if (!this._matrixClient || payload.member.userId !== this._matrixClient.credentials.userId) break;
|
||||||
|
this._generateRoomLists();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'RoomListActions.tagRoom.pending': {
|
||||||
|
// XXX: we only show one optimistic update at any one time.
|
||||||
|
// Ideally we should be making a list of in-flight requests
|
||||||
|
// that are backed by transaction IDs. Until the js-sdk
|
||||||
|
// supports this, we're stuck with only being able to use
|
||||||
|
// the most recent optimistic update.
|
||||||
|
this._generateRoomLists(payload.request);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'RoomListActions.tagRoom.failure': {
|
||||||
|
// Reset state according to js-sdk
|
||||||
|
this._generateRoomLists();
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
this._init();
|
||||||
|
this._matrixClient = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateRoomLists(optimisticRequest) {
|
||||||
|
const lists = {
|
||||||
|
"im.vector.fake.invite": [],
|
||||||
|
"m.favourite": [],
|
||||||
|
"im.vector.fake.recent": [],
|
||||||
|
"im.vector.fake.direct": [],
|
||||||
|
"m.lowpriority": [],
|
||||||
|
"im.vector.fake.archived": [],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const dmRoomMap = DMRoomMap.shared();
|
||||||
|
|
||||||
|
// If somehow we dispatched a RoomListActions.tagRoom.failure before a MatrixActions.sync
|
||||||
|
if (!this._matrixClient) return;
|
||||||
|
|
||||||
|
this._matrixClient.getRooms().forEach((room, index) => {
|
||||||
|
const me = room.getMember(this._matrixClient.credentials.userId);
|
||||||
|
if (!me) return;
|
||||||
|
|
||||||
|
if (me.membership == "invite") {
|
||||||
|
lists["im.vector.fake.invite"].push(room);
|
||||||
|
} 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
|
||||||
|
let tagNames = Object.keys(room.tags);
|
||||||
|
|
||||||
|
if (optimisticRequest && optimisticRequest.room === room) {
|
||||||
|
// Remove old tag
|
||||||
|
tagNames = tagNames.filter((tagName) => tagName !== optimisticRequest.oldTag);
|
||||||
|
// Add new tag
|
||||||
|
if (optimisticRequest.newTag &&
|
||||||
|
!tagNames.includes(optimisticRequest.newTag)
|
||||||
|
) {
|
||||||
|
tagNames.push(optimisticRequest.newTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagNames.length) {
|
||||||
|
for (let i = 0; i < tagNames.length; i++) {
|
||||||
|
const tagName = tagNames[i];
|
||||||
|
lists[tagName] = lists[tagName] || [];
|
||||||
|
lists[tagName].push(room);
|
||||||
|
}
|
||||||
|
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
|
||||||
|
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
|
||||||
|
lists["im.vector.fake.direct"].push(room);
|
||||||
|
} else {
|
||||||
|
lists["im.vector.fake.recent"].push(room);
|
||||||
|
}
|
||||||
|
} else if (me.membership === "leave") {
|
||||||
|
lists["im.vector.fake.archived"].push(room);
|
||||||
|
} else {
|
||||||
|
console.error("unrecognised membership: " + me.membership + " - this should never happen");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const listOrders = {
|
||||||
|
"manual": [
|
||||||
|
"m.favourite",
|
||||||
|
],
|
||||||
|
"recent": [
|
||||||
|
"im.vector.fake.invite",
|
||||||
|
"im.vector.fake.recent",
|
||||||
|
"im.vector.fake.direct",
|
||||||
|
"m.lowpriority",
|
||||||
|
"im.vector.fake.archived",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(listOrders).forEach((order) => {
|
||||||
|
listOrders[order].forEach((listKey) => {
|
||||||
|
let comparator;
|
||||||
|
switch (order) {
|
||||||
|
case "manual":
|
||||||
|
comparator = this._getManualComparator(listKey, optimisticRequest);
|
||||||
|
break;
|
||||||
|
case "recent":
|
||||||
|
comparator = this._recentsComparator;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lists[listKey].sort(comparator);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._setState({
|
||||||
|
lists,
|
||||||
|
ready: true, // Ready to receive updates via Room.tags events
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_tsOfNewestEvent(room) {
|
||||||
|
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
||||||
|
const ev = room.timeline[i];
|
||||||
|
if (ev.getTs() &&
|
||||||
|
(Unread.eventTriggersUnreadCount(ev) ||
|
||||||
|
(ev.getSender() === this._matrixClient.credentials.userId))
|
||||||
|
) {
|
||||||
|
return ev.getTs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we might only have events that don't trigger the unread indicator,
|
||||||
|
// in which case use the oldest event even if normally it wouldn't count.
|
||||||
|
// This is better than just assuming the last event was forever ago.
|
||||||
|
if (room.timeline.length && room.timeline[0].getTs()) {
|
||||||
|
return room.timeline[0].getTs();
|
||||||
|
} else {
|
||||||
|
return Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_recentsComparator(roomA, roomB) {
|
||||||
|
return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA);
|
||||||
|
}
|
||||||
|
|
||||||
|
_lexicographicalComparator(roomA, roomB) {
|
||||||
|
return roomA.name > roomB.name ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getManualComparator(tagName, optimisticRequest) {
|
||||||
|
return (roomA, roomB) => {
|
||||||
|
let metaA = roomA.tags[tagName];
|
||||||
|
let metaB = roomB.tags[tagName];
|
||||||
|
|
||||||
|
if (optimisticRequest && roomA === optimisticRequest.room) metaA = optimisticRequest.metaData;
|
||||||
|
if (optimisticRequest && roomB === optimisticRequest.room) metaB = optimisticRequest.metaData;
|
||||||
|
|
||||||
|
// Make sure the room tag has an order element, if not set it to be the bottom
|
||||||
|
const a = metaA.order;
|
||||||
|
const b = metaB.order;
|
||||||
|
|
||||||
|
// Order undefined room tag orders to the bottom
|
||||||
|
if (a === undefined && b !== undefined) {
|
||||||
|
return 1;
|
||||||
|
} else if (a !== undefined && b === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a == b ? this._lexicographicalComparator(roomA, roomB) : ( a > b ? 1 : -1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomLists() {
|
||||||
|
return this._state.lists;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.singletonRoomListStore === undefined) {
|
||||||
|
global.singletonRoomListStore = new RoomListStore();
|
||||||
|
}
|
||||||
|
export default global.singletonRoomListStore;
|
Loading…
Reference in a new issue