Merge branch 'develop' into luke/feature-tag-panel-tile-context-menu
						commit
						a34fea8af8
					
				|  | @ -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 | ||||
|  * the given MatrixClient. | ||||
|  | @ -78,6 +86,8 @@ export default { | |||
|     start(matrixClient) { | ||||
|         this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); | ||||
|         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) { | ||||
|         const listener = (...args) => { | ||||
|             dis.dispatch(actionCreator(matrixClient, ...args)); | ||||
|             dis.dispatch(actionCreator(matrixClient, ...args), true); | ||||
|         }; | ||||
|         matrixClient.on(eventName, listener); | ||||
|         this._matrixClientListenersStop.push(() => { | ||||
|  |  | |||
|  | @ -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}.success` or | ||||
|  *                         `${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) { | ||||
|     return (dispatch) => { | ||||
|  |  | |||
|  | @ -208,7 +208,6 @@ const LoggedInView = 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'); | ||||
|  | @ -330,7 +329,6 @@ const LoggedInView = React.createClass({ | |||
|             <div className='mx_MatrixChat_wrapper'> | ||||
|                 { topBar } | ||||
|                 <div className={bodyClasses}> | ||||
|                     { SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> } | ||||
|                     <LeftPanel | ||||
|                         collapsed={this.props.collapseLhs || false} | ||||
|                         disabled={this.props.leftDisabled} | ||||
|  |  | |||
|  | @ -20,12 +20,11 @@ import { MatrixClient } from 'matrix-js-sdk'; | |||
| import TagOrderStore from '../../stores/TagOrderStore'; | ||||
| 
 | ||||
| import GroupActions from '../../actions/GroupActions'; | ||||
| import TagOrderActions from '../../actions/TagOrderActions'; | ||||
| 
 | ||||
| import sdk from '../../index'; | ||||
| import dis from '../../dispatcher'; | ||||
| 
 | ||||
| import { DragDropContext, Droppable } from 'react-beautiful-dnd'; | ||||
| import { Droppable } from 'react-beautiful-dnd'; | ||||
| 
 | ||||
| const TagPanel = React.createClass({ | ||||
|     displayName: 'TagPanel', | ||||
|  | @ -94,25 +93,8 @@ const TagPanel = React.createClass({ | |||
|         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() { | ||||
|         const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); | ||||
|         const TintableSvg = sdk.getComponent('elements.TintableSvg'); | ||||
|         const GroupsButton = sdk.getComponent('elements.GroupsButton'); | ||||
|         const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); | ||||
| 
 | ||||
|         const tags = this.state.orderedTags.map((tag, index) => { | ||||
|  | @ -124,27 +106,28 @@ const TagPanel = React.createClass({ | |||
|             />; | ||||
|         }); | ||||
|         return <div className="mx_TagPanel"> | ||||
|             <DragDropContext onDragEnd={this.onTagTileEndDrag}> | ||||
|                 <Droppable droppableId="tag-panel-droppable"> | ||||
|                     { (provided, snapshot) => ( | ||||
|                         <div | ||||
|                             className="mx_TagPanel_tagTileContainer" | ||||
|                             ref={provided.innerRef} | ||||
|                             // react-beautiful-dnd has a bug that emits a click to the parent
 | ||||
|                             // of draggables upon dropping
 | ||||
|                             //   https://github.com/atlassian/react-beautiful-dnd/issues/273
 | ||||
|                             // so we use onMouseDown here as a workaround.
 | ||||
|                             onMouseDown={this.onClick} | ||||
|                         > | ||||
|                             { tags } | ||||
|                             { provided.placeholder } | ||||
|                         </div> | ||||
|                     ) } | ||||
|                 </Droppable> | ||||
|             </DragDropContext> | ||||
|             <AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}> | ||||
|                 <TintableSvg src="img/icons-create-room.svg" width="25" height="25" /> | ||||
|             </AccessibleButton> | ||||
|             <Droppable | ||||
|                 droppableId="tag-panel-droppable" | ||||
|                 type="draggable-TagTile" | ||||
|             > | ||||
|                 { (provided, snapshot) => ( | ||||
|                     <div | ||||
|                         className="mx_TagPanel_tagTileContainer" | ||||
|                         ref={provided.innerRef} | ||||
|                         // react-beautiful-dnd has a bug that emits a click to the parent
 | ||||
|                         // of draggables upon dropping
 | ||||
|                         //   https://github.com/atlassian/react-beautiful-dnd/issues/273
 | ||||
|                         // so we use onMouseDown here as a workaround.
 | ||||
|                         onMouseDown={this.onClick} | ||||
|                     > | ||||
|                         { tags } | ||||
|                         { provided.placeholder } | ||||
|                     </div> | ||||
|                 ) } | ||||
|             </Droppable> | ||||
|             <div className="mx_TagPanel_createGroupButton"> | ||||
|                 <GroupsButton tooltip={true} /> | ||||
|             </div> | ||||
|         </div>; | ||||
|     }, | ||||
| }); | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ export default function DNDTagTile(props) { | |||
|             key={props.tag} | ||||
|             draggableId={props.tag} | ||||
|             index={props.index} | ||||
|             type="draggable-TagTile" | ||||
|         > | ||||
|             { (provided, snapshot) => ( | ||||
|                 <div> | ||||
|  |  | |||
|  | @ -18,7 +18,6 @@ limitations under the License. | |||
| 'use strict'; | ||||
| const React = require("react"); | ||||
| const ReactDOM = require("react-dom"); | ||||
| import { DragDropContext } from 'react-beautiful-dnd'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { _t } from '../../../languageHandler'; | ||||
| const GeminiScrollbar = require('react-gemini-scrollbar'); | ||||
|  | @ -27,14 +26,13 @@ const CallHandler = require('../../../CallHandler'); | |||
| const dis = require("../../../dispatcher"); | ||||
| const sdk = require('../../../index'); | ||||
| const rate_limited_func = require('../../../ratelimitedfunc'); | ||||
| const Rooms = require('../../../Rooms'); | ||||
| import * as Rooms from '../../../Rooms'; | ||||
| import DMRoomMap from '../../../utils/DMRoomMap'; | ||||
| const Receipt = require('../../../utils/Receipt'); | ||||
| import TagOrderStore from '../../../stores/TagOrderStore'; | ||||
| import RoomListStore from '../../../stores/RoomListStore'; | ||||
| import GroupStoreCache from '../../../stores/GroupStoreCache'; | ||||
| 
 | ||||
| import Modal from '../../../Modal'; | ||||
| 
 | ||||
| const HIDE_CONFERENCE_CHANS = true; | ||||
| 
 | ||||
| function phraseForSection(section) { | ||||
|  | @ -80,7 +78,6 @@ module.exports = React.createClass({ | |||
|         cli.on("deleteRoom", this.onDeleteRoom); | ||||
|         cli.on("Room.timeline", this.onRoomTimeline); | ||||
|         cli.on("Room.name", this.onRoomName); | ||||
|         cli.on("Room.tags", this.onRoomTags); | ||||
|         cli.on("Room.receipt", this.onRoomReceipt); | ||||
|         cli.on("RoomState.events", this.onRoomStateEvents); | ||||
|         cli.on("RoomMember.name", this.onRoomMemberName); | ||||
|  | @ -118,6 +115,10 @@ module.exports = React.createClass({ | |||
|             this.updateVisibleRooms(); | ||||
|         }); | ||||
| 
 | ||||
|         this._roomListStoreToken = RoomListStore.addListener(() => { | ||||
|             this._delayedRefreshRoomList(); | ||||
|         }); | ||||
| 
 | ||||
|         this.refreshRoomList(); | ||||
| 
 | ||||
|         // order of the sublists
 | ||||
|  | @ -178,7 +179,6 @@ module.exports = React.createClass({ | |||
|             MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom); | ||||
|             MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); | ||||
|             MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); | ||||
|             MatrixClientPeg.get().removeListener("Room.tags", this.onRoomTags); | ||||
|             MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); | ||||
|             MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); | ||||
|             MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName); | ||||
|  | @ -251,10 +251,6 @@ module.exports = React.createClass({ | |||
|         this._delayedRefreshRoomList(); | ||||
|     }, | ||||
| 
 | ||||
|     onRoomTags: function(event, room) { | ||||
|         this._delayedRefreshRoomList(); | ||||
|     }, | ||||
| 
 | ||||
|     onRoomStateEvents: function(ev, state) { | ||||
|         this._delayedRefreshRoomList(); | ||||
|     }, | ||||
|  | @ -278,106 +274,6 @@ module.exports = React.createClass({ | |||
|         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() { | ||||
|         this.refreshRoomList(); | ||||
|     }, 500), | ||||
|  | @ -441,7 +337,7 @@ module.exports = React.createClass({ | |||
|             totalRooms += l.length; | ||||
|         } | ||||
|         this.setState({ | ||||
|             lists: this.getRoomLists(), | ||||
|             lists, | ||||
|             totalRoomCount: totalRooms, | ||||
|             // Do this here so as to not render every time the selected tags
 | ||||
|             // themselves change.
 | ||||
|  | @ -452,70 +348,34 @@ module.exports = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     getRoomLists: function() { | ||||
|         const lists = {}; | ||||
|         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 lists = RoomListStore.getRoomLists(); | ||||
| 
 | ||||
|         const dmRoomMap = DMRoomMap.shared(); | ||||
|         const filteredLists = {}; | ||||
| 
 | ||||
|         this._visibleRooms.forEach((room, index) => { | ||||
|             const me = room.getMember(MatrixClientPeg.get().credentials.userId); | ||||
|             if (!me) return; | ||||
|         const isRoomVisible = { | ||||
|             // $roomId: true,
 | ||||
|         }; | ||||
| 
 | ||||
|             // console.log("room = " + room.name + ", me.membership = " + me.membership +
 | ||||
|             //             ", sender = " + me.events.member.getSender() +
 | ||||
|             //             ", 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"); | ||||
|             } | ||||
|         this._visibleRooms.forEach((r) => { | ||||
|             isRoomVisible[r.roomId] = true; | ||||
|         }); | ||||
| 
 | ||||
|         // 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
 | ||||
| /* | ||||
|         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 Boolean(isRoomVisible[taggedRoom.roomId]); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         return lists; | ||||
|         return filteredLists; | ||||
|     }, | ||||
| 
 | ||||
|     _getScrollNode: function() { | ||||
|  | @ -752,116 +612,114 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|         const self = this; | ||||
|         return ( | ||||
|             <DragDropContext onDragEnd={this.onRoomTileEndDrag}> | ||||
|                 <GeminiScrollbar className="mx_RoomList_scrollbar" | ||||
|                      autoshow={true} onScroll={self._whenScrolling} ref="gemscroll"> | ||||
|                 <div className="mx_RoomList"> | ||||
|                     <RoomSubList list={[]} | ||||
|                                  extraTiles={this._makeGroupInviteTiles()} | ||||
|                                  label={_t('Community Invites')} | ||||
|                                  editable={false} | ||||
|                                  order="recent" | ||||
|                                  isInvite={true} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} | ||||
|                     /> | ||||
|             <GeminiScrollbar className="mx_RoomList_scrollbar" | ||||
|                  autoshow={true} onScroll={self._whenScrolling} ref="gemscroll"> | ||||
|             <div className="mx_RoomList"> | ||||
|                 <RoomSubList list={[]} | ||||
|                              extraTiles={this._makeGroupInviteTiles()} | ||||
|                              label={_t('Community Invites')} | ||||
|                              editable={false} | ||||
|                              order="recent" | ||||
|                              isInvite={true} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} | ||||
|                 /> | ||||
| 
 | ||||
|                     <RoomSubList list={self.state.lists['im.vector.fake.invite']} | ||||
|                                  label={_t('Invites')} | ||||
|                                  editable={false} | ||||
|                                  order="recent" | ||||
|                                  isInvite={true} | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} | ||||
|                     /> | ||||
|                 <RoomSubList list={self.state.lists['im.vector.fake.invite']} | ||||
|                              label={_t('Invites')} | ||||
|                              editable={false} | ||||
|                              order="recent" | ||||
|                              isInvite={true} | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} | ||||
|                 /> | ||||
| 
 | ||||
|                     <RoomSubList list={self.state.lists['m.favourite']} | ||||
|                                  label={_t('Favourites')} | ||||
|                                  tagName="m.favourite" | ||||
|                                  emptyContent={this._getEmptyContent('m.favourite')} | ||||
|                                  editable={true} | ||||
|                                  order="manual" | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} /> | ||||
|                 <RoomSubList list={self.state.lists['m.favourite']} | ||||
|                              label={_t('Favourites')} | ||||
|                              tagName="m.favourite" | ||||
|                              emptyContent={this._getEmptyContent('m.favourite')} | ||||
|                              editable={true} | ||||
|                              order="manual" | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} /> | ||||
| 
 | ||||
|                     <RoomSubList list={self.state.lists['im.vector.fake.direct']} | ||||
|                                  label={_t('People')} | ||||
|                                  tagName="im.vector.fake.direct" | ||||
|                                  emptyContent={this._getEmptyContent('im.vector.fake.direct')} | ||||
|                                  headerItems={this._getHeaderItems('im.vector.fake.direct')} | ||||
|                                  editable={true} | ||||
|                                  order="recent" | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  alwaysShowHeader={true} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} /> | ||||
|                 <RoomSubList list={self.state.lists['im.vector.fake.direct']} | ||||
|                              label={_t('People')} | ||||
|                              tagName="im.vector.fake.direct" | ||||
|                              emptyContent={this._getEmptyContent('im.vector.fake.direct')} | ||||
|                              headerItems={this._getHeaderItems('im.vector.fake.direct')} | ||||
|                              editable={true} | ||||
|                              order="recent" | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              alwaysShowHeader={true} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} /> | ||||
| 
 | ||||
|                     <RoomSubList list={self.state.lists['im.vector.fake.recent']} | ||||
|                                  label={_t('Rooms')} | ||||
|                                  editable={true} | ||||
|                                  emptyContent={this._getEmptyContent('im.vector.fake.recent')} | ||||
|                                  headerItems={this._getHeaderItems('im.vector.fake.recent')} | ||||
|                                  order="recent" | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} /> | ||||
|                 <RoomSubList list={self.state.lists['im.vector.fake.recent']} | ||||
|                              label={_t('Rooms')} | ||||
|                              editable={true} | ||||
|                              emptyContent={this._getEmptyContent('im.vector.fake.recent')} | ||||
|                              headerItems={this._getHeaderItems('im.vector.fake.recent')} | ||||
|                              order="recent" | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} /> | ||||
| 
 | ||||
|                     { Object.keys(self.state.lists).map((tagName) => { | ||||
|                         if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { | ||||
|                             return <RoomSubList list={self.state.lists[tagName]} | ||||
|                                  key={tagName} | ||||
|                                  label={tagName} | ||||
|                                  tagName={tagName} | ||||
|                                  emptyContent={this._getEmptyContent(tagName)} | ||||
|                                  editable={true} | ||||
|                                  order="manual" | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} />; | ||||
|                         } | ||||
|                     }) } | ||||
|                 { Object.keys(self.state.lists).map((tagName) => { | ||||
|                     if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { | ||||
|                         return <RoomSubList list={self.state.lists[tagName]} | ||||
|                              key={tagName} | ||||
|                              label={tagName} | ||||
|                              tagName={tagName} | ||||
|                              emptyContent={this._getEmptyContent(tagName)} | ||||
|                              editable={true} | ||||
|                              order="manual" | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} />; | ||||
|                     } | ||||
|                 }) } | ||||
| 
 | ||||
|                     <RoomSubList list={self.state.lists['m.lowpriority']} | ||||
|                                  label={_t('Low priority')} | ||||
|                                  tagName="m.lowpriority" | ||||
|                                  emptyContent={this._getEmptyContent('m.lowpriority')} | ||||
|                                  editable={true} | ||||
|                                  order="recent" | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onHeaderClick={self.onSubListHeaderClick} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} /> | ||||
|                 <RoomSubList list={self.state.lists['m.lowpriority']} | ||||
|                              label={_t('Low priority')} | ||||
|                              tagName="m.lowpriority" | ||||
|                              emptyContent={this._getEmptyContent('m.lowpriority')} | ||||
|                              editable={true} | ||||
|                              order="recent" | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onHeaderClick={self.onSubListHeaderClick} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} /> | ||||
| 
 | ||||
|                     <RoomSubList list={self.state.lists['im.vector.fake.archived']} | ||||
|                                  label={_t('Historical')} | ||||
|                                  editable={false} | ||||
|                                  order="recent" | ||||
|                                  collapsed={self.props.collapsed} | ||||
|                                  alwaysShowHeader={true} | ||||
|                                  startAsHidden={true} | ||||
|                                  showSpinner={self.state.isLoadingLeftRooms} | ||||
|                                  onHeaderClick= {self.onArchivedHeaderClick} | ||||
|                                  incomingCall={self.state.incomingCall} | ||||
|                                  searchFilter={self.props.searchFilter} | ||||
|                                  onShowMoreRooms={self.onShowMoreRooms} /> | ||||
|                 </div> | ||||
|                 </GeminiScrollbar> | ||||
|             </DragDropContext> | ||||
|                 <RoomSubList list={self.state.lists['im.vector.fake.archived']} | ||||
|                              label={_t('Historical')} | ||||
|                              editable={false} | ||||
|                              order="recent" | ||||
|                              collapsed={self.props.collapsed} | ||||
|                              alwaysShowHeader={true} | ||||
|                              startAsHidden={true} | ||||
|                              showSpinner={self.state.isLoadingLeftRooms} | ||||
|                              onHeaderClick= {self.onArchivedHeaderClick} | ||||
|                              incomingCall={self.state.incomingCall} | ||||
|                              searchFilter={self.props.searchFilter} | ||||
|                              onShowMoreRooms={self.onShowMoreRooms} /> | ||||
|             </div> | ||||
|             </GeminiScrollbar> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
|  |  | |||
|  | @ -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 New Issue
	
	 Luke Barnard
						Luke Barnard