Merge branches 'develop' and 't3chguy/m.relates_to' of github.com:matrix-org/matrix-react-sdk into t3chguy/m.relates_to
This commit is contained in:
commit
2e3cbb309e
20 changed files with 837 additions and 350 deletions
|
@ -50,11 +50,15 @@ function pad(n) {
|
||||||
return (n < 10 ? '0' : '') + n;
|
return (n < 10 ? '0' : '') + n;
|
||||||
}
|
}
|
||||||
|
|
||||||
function twelveHourTime(date) {
|
function twelveHourTime(date, showSeconds=false) {
|
||||||
let hours = date.getHours() % 12;
|
let hours = date.getHours() % 12;
|
||||||
const minutes = pad(date.getMinutes());
|
const minutes = pad(date.getMinutes());
|
||||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||||
hours = hours ? hours : 12; // convert 0 -> 12
|
hours = hours ? hours : 12; // convert 0 -> 12
|
||||||
|
if (showSeconds) {
|
||||||
|
const seconds = pad(date.getSeconds());
|
||||||
|
return `${hours}:${minutes}:${seconds}${ampm}`;
|
||||||
|
}
|
||||||
return `${hours}:${minutes}${ampm}`;
|
return `${hours}:${minutes}${ampm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,10 +105,17 @@ export function formatFullDate(date, showTwelveHour=false) {
|
||||||
monthName: months[date.getMonth()],
|
monthName: months[date.getMonth()],
|
||||||
day: date.getDate(),
|
day: date.getDate(),
|
||||||
fullYear: date.getFullYear(),
|
fullYear: date.getFullYear(),
|
||||||
time: formatTime(date, showTwelveHour),
|
time: formatFullTime(date, showTwelveHour),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatFullTime(date, showTwelveHour=false) {
|
||||||
|
if (showTwelveHour) {
|
||||||
|
return twelveHourTime(date, true);
|
||||||
|
}
|
||||||
|
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
export function formatTime(date, showTwelveHour=false) {
|
export function formatTime(date, showTwelveHour=false) {
|
||||||
if (showTwelveHour) {
|
if (showTwelveHour) {
|
||||||
return twelveHourTime(date);
|
return twelveHourTime(date);
|
||||||
|
|
|
@ -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;
|
|
@ -35,6 +35,7 @@ const TagOrderActions = {};
|
||||||
TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
||||||
// Only commit tags if the state is ready, i.e. not null
|
// Only commit tags if the state is ready, i.e. not null
|
||||||
let tags = TagOrderStore.getOrderedTags();
|
let tags = TagOrderStore.getOrderedTags();
|
||||||
|
let removedTags = TagOrderStore.getRemovedTagsAccountData();
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -42,17 +43,66 @@ TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
||||||
tags = tags.filter((t) => t !== tag);
|
tags = tags.filter((t) => t !== tag);
|
||||||
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
||||||
|
|
||||||
|
removedTags = removedTags.filter((t) => t !== tag);
|
||||||
|
|
||||||
const storeId = TagOrderStore.getStoreId();
|
const storeId = TagOrderStore.getStoreId();
|
||||||
|
|
||||||
return asyncAction('TagOrderActions.moveTag', () => {
|
return asyncAction('TagOrderActions.moveTag', () => {
|
||||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||||
return matrixClient.setAccountData(
|
return matrixClient.setAccountData(
|
||||||
'im.vector.web.tag_ordering',
|
'im.vector.web.tag_ordering',
|
||||||
{tags, _storeId: storeId},
|
{tags, removedTags, _storeId: storeId},
|
||||||
);
|
);
|
||||||
}, () => {
|
}, () => {
|
||||||
// For an optimistic update
|
// For an optimistic update
|
||||||
return {tags};
|
return {tags, removedTags};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an action thunk that will do an asynchronous request to
|
||||||
|
* label a tag as removed in im.vector.web.tag_ordering account data.
|
||||||
|
*
|
||||||
|
* The reason this is implemented with new state `removedTags` is that
|
||||||
|
* we incrementally and initially populate `tags` with groups that
|
||||||
|
* have been joined. If we remove a group from `tags`, it will just
|
||||||
|
* get added (as it looks like a group we've recently joined).
|
||||||
|
*
|
||||||
|
* NB: If we ever support adding of tags (which is planned), we should
|
||||||
|
* take special care to remove the tag from `removedTags` when we add
|
||||||
|
* it.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||||
|
* account data on.
|
||||||
|
* @param {string} tag the tag to remove.
|
||||||
|
* @returns {function} an action thunk that will dispatch actions
|
||||||
|
* indicating the status of the request.
|
||||||
|
* @see asyncAction
|
||||||
|
*/
|
||||||
|
TagOrderActions.removeTag = function(matrixClient, tag) {
|
||||||
|
// Don't change tags, just removedTags
|
||||||
|
const tags = TagOrderStore.getOrderedTags();
|
||||||
|
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||||
|
|
||||||
|
if (removedTags.includes(tag)) {
|
||||||
|
// Return a thunk that doesn't do anything, we don't even need
|
||||||
|
// an asynchronous action here, the tag is already removed.
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
removedTags.push(tag);
|
||||||
|
|
||||||
|
const storeId = TagOrderStore.getStoreId();
|
||||||
|
|
||||||
|
return asyncAction('TagOrderActions.removeTag', () => {
|
||||||
|
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||||
|
return matrixClient.setAccountData(
|
||||||
|
'im.vector.web.tag_ordering',
|
||||||
|
{tags, removedTags, _storeId: storeId},
|
||||||
|
);
|
||||||
|
}, () => {
|
||||||
|
// For an optimistic update
|
||||||
|
return {removedTags};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -158,7 +158,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
{ completions }
|
{ completions }
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import * as Matrix from 'matrix-js-sdk';
|
import * as Matrix from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
import Notifier from '../../Notifier';
|
import Notifier from '../../Notifier';
|
||||||
|
@ -30,6 +31,9 @@ import sessionStore from '../../stores/SessionStore';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
|
import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
|
import RoomListActions from '../../actions/RoomListActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||||
* determined by the page_type property.
|
* determined by the page_type property.
|
||||||
|
@ -207,8 +211,51 @@ const LoggedInView = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onDragEnd: function(result) {
|
||||||
|
// Dragged to an invalid destination, not onto a droppable
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = result.destination.droppableId;
|
||||||
|
|
||||||
|
if (dest === 'tag-panel-droppable') {
|
||||||
|
// Could be "GroupTile +groupId:domain"
|
||||||
|
const draggableId = result.draggableId.split(' ').pop();
|
||||||
|
|
||||||
|
// Dispatch synchronously so that the TagPanel receives an
|
||||||
|
// optimistic update from TagOrderStore before the previous
|
||||||
|
// state is shown.
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(
|
||||||
|
this._matrixClient,
|
||||||
|
draggableId,
|
||||||
|
result.destination.index,
|
||||||
|
), true);
|
||||||
|
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||||
|
this._onRoomTileEndDrag(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRoomTileEndDrag: function(result) {
|
||||||
|
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 oldIndex = result.source.index;
|
||||||
|
const newIndex = result.destination.index;
|
||||||
|
|
||||||
|
dis.dispatch(RoomListActions.tagRoom(
|
||||||
|
this._matrixClient,
|
||||||
|
this._matrixClient.getRoom(roomId),
|
||||||
|
prevTag, newTag,
|
||||||
|
oldIndex, newIndex,
|
||||||
|
), true);
|
||||||
|
},
|
||||||
|
|
||||||
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');
|
||||||
|
@ -329,17 +376,18 @@ const LoggedInView = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className='mx_MatrixChat_wrapper'>
|
<div className='mx_MatrixChat_wrapper'>
|
||||||
{ topBar }
|
{ topBar }
|
||||||
<div className={bodyClasses}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
<div className={bodyClasses}>
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
collapsed={this.props.collapseLhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
disabled={this.props.leftDisabled}
|
disabled={this.props.leftDisabled}
|
||||||
/>
|
/>
|
||||||
<main className='mx_MatrixChat_middlePanel'>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
{ page_element }
|
{ page_element }
|
||||||
</main>
|
</main>
|
||||||
{ right_panel }
|
{ right_panel }
|
||||||
</div>
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -73,8 +73,10 @@ export default withMatrixClient(React.createClass({
|
||||||
});
|
});
|
||||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||||
content = groupNodes.length > 0 ?
|
content = groupNodes.length > 0 ?
|
||||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
<GeminiScrollbar>
|
||||||
{ groupNodes }
|
<div className="mx_MyGroups_joinedGroups">
|
||||||
|
{ groupNodes }
|
||||||
|
</div>
|
||||||
</GeminiScrollbar> :
|
</GeminiScrollbar> :
|
||||||
<div className="mx_MyGroups_placeholder">
|
<div className="mx_MyGroups_placeholder">
|
||||||
{ _t(
|
{ _t(
|
||||||
|
|
|
@ -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,27 +106,28 @@ 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"
|
||||||
{ (provided, snapshot) => (
|
type="draggable-TagTile"
|
||||||
<div
|
>
|
||||||
className="mx_TagPanel_tagTileContainer"
|
{ (provided, snapshot) => (
|
||||||
ref={provided.innerRef}
|
<div
|
||||||
// react-beautiful-dnd has a bug that emits a click to the parent
|
className="mx_TagPanel_tagTileContainer"
|
||||||
// of draggables upon dropping
|
ref={provided.innerRef}
|
||||||
// https://github.com/atlassian/react-beautiful-dnd/issues/273
|
// react-beautiful-dnd has a bug that emits a click to the parent
|
||||||
// so we use onMouseDown here as a workaround.
|
// of draggables upon dropping
|
||||||
onMouseDown={this.onClick}
|
// https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||||
>
|
// so we use onMouseDown here as a workaround.
|
||||||
{ tags }
|
onMouseDown={this.onClick}
|
||||||
{ provided.placeholder }
|
>
|
||||||
</div>
|
{ tags }
|
||||||
) }
|
{ provided.placeholder }
|
||||||
</Droppable>
|
</div>
|
||||||
</DragDropContext>
|
) }
|
||||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
</Droppable>
|
||||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
<div className="mx_TagPanel_createGroupButton">
|
||||||
</AccessibleButton>
|
<GroupsButton tooltip={true} />
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1121,9 +1121,9 @@ var TimelinePanel = React.createClass({
|
||||||
// exist.
|
// exist.
|
||||||
if (this.state.timelineLoading) {
|
if (this.state.timelineLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
<div className="mx_RoomView_messagePanelSpinner">
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -431,10 +431,10 @@ module.exports = React.createClass({
|
||||||
// FIXME: remove status.im theme tweaks
|
// FIXME: remove status.im theme tweaks
|
||||||
const theme = SettingsStore.getValue("theme");
|
const theme = SettingsStore.getValue("theme");
|
||||||
if (theme !== "status") {
|
if (theme !== "status") {
|
||||||
header = <h2>{ _t('Sign in') }</h2>;
|
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||||
} else {
|
} else {
|
||||||
if (!this.state.errorText) {
|
if (!this.state.errorText) {
|
||||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||||
|
import ContextualMenu from '../../structures/ContextualMenu';
|
||||||
|
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
|
|
||||||
|
@ -81,6 +82,35 @@ export default React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onContextButtonClick: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Hide the (...) immediately
|
||||||
|
this.setState({ hover: false });
|
||||||
|
|
||||||
|
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||||
|
const elementRect = e.target.getBoundingClientRect();
|
||||||
|
|
||||||
|
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||||
|
const x = elementRect.right + window.pageXOffset + 3;
|
||||||
|
const chevronOffset = 12;
|
||||||
|
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||||
|
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||||
|
chevronOffset: chevronOffset,
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
tag: this.props.tag,
|
||||||
|
onFinished: function() {
|
||||||
|
self.setState({ menuDisplayed: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.setState({ menuDisplayed: true });
|
||||||
|
},
|
||||||
|
|
||||||
onMouseOver: function() {
|
onMouseOver: function() {
|
||||||
this.setState({hover: true});
|
this.setState({hover: true});
|
||||||
},
|
},
|
||||||
|
@ -109,10 +139,15 @@ export default React.createClass({
|
||||||
const tip = this.state.hover ?
|
const tip = this.state.hover ?
|
||||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||||
<div />;
|
<div />;
|
||||||
|
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||||
|
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||||
|
{ "\u00B7\u00B7\u00B7" }
|
||||||
|
</div> : <div />;
|
||||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||||
{ tip }
|
{ tip }
|
||||||
|
{ contextButton }
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,10 +17,12 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClient} from 'matrix-js-sdk';
|
import {MatrixClient} from 'matrix-js-sdk';
|
||||||
|
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
|
|
||||||
|
|
||||||
const GroupTile = React.createClass({
|
const GroupTile = React.createClass({
|
||||||
displayName: 'GroupTile',
|
displayName: 'GroupTile',
|
||||||
|
|
||||||
|
@ -78,9 +80,39 @@ const GroupTile = React.createClass({
|
||||||
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
|
||||||
) : null;
|
) : null;
|
||||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||||
<div className="mx_GroupTile_avatar">
|
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||||
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
{ (droppableProvided, droppableSnapshot) => (
|
||||||
</div>
|
<div ref={droppableProvided.innerRef}>
|
||||||
|
<Draggable
|
||||||
|
key={"GroupTile " + this.props.groupId}
|
||||||
|
draggableId={"GroupTile " + this.props.groupId}
|
||||||
|
index={this.props.groupId}
|
||||||
|
type="draggable-TagTile"
|
||||||
|
>
|
||||||
|
{ (provided, snapshot) => (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
>
|
||||||
|
<div className="mx_GroupTile_avatar">
|
||||||
|
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
|
||||||
|
{ provided.placeholder ?
|
||||||
|
<div className="mx_GroupTile_avatar">
|
||||||
|
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
|
||||||
|
</div> :
|
||||||
|
<div />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
) }
|
||||||
|
</Droppable>
|
||||||
<div className="mx_GroupTile_profile">
|
<div className="mx_GroupTile_profile">
|
||||||
<div className="mx_GroupTile_name">{ name }</div>
|
<div className="mx_GroupTile_name">{ name }</div>
|
||||||
{ descElement }
|
{ descElement }
|
||||||
|
|
|
@ -82,7 +82,7 @@ Tinter.registerTintable(updateTintedDownloadImage);
|
||||||
// downloaded. This limit does not seem to apply when the url is used as
|
// downloaded. This limit does not seem to apply when the url is used as
|
||||||
// the source attribute of an image tag.
|
// the source attribute of an image tag.
|
||||||
//
|
//
|
||||||
// Blob URLs are generated using window.URL.createObjectURL and unforuntately
|
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
|
||||||
// for our purposes they inherit the origin of the page that created them.
|
// for our purposes they inherit the origin of the page that created them.
|
||||||
// This means that any scripts that run when the URL is viewed will be able
|
// This means that any scripts that run when the URL is viewed will be able
|
||||||
// to access local storage.
|
// to access local storage.
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -191,6 +191,10 @@ module.exports = React.createClass({
|
||||||
this._tagStoreToken.remove();
|
this._tagStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._roomListStoreToken) {
|
||||||
|
this._roomListStoreToken.remove();
|
||||||
|
}
|
||||||
|
|
||||||
if (this._groupStoreTokens.length > 0) {
|
if (this._groupStoreTokens.length > 0) {
|
||||||
// NB: GroupStore is not a Flux.Store
|
// NB: GroupStore is not a Flux.Store
|
||||||
this._groupStoreTokens.forEach((token) => token.unregister());
|
this._groupStoreTokens.forEach((token) => token.unregister());
|
||||||
|
@ -251,10 +255,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 +278,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 +341,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 +352,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,116 +616,114 @@ 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">
|
<RoomSubList list={[]}
|
||||||
<RoomSubList list={[]}
|
extraTiles={this._makeGroupInviteTiles()}
|
||||||
extraTiles={this._makeGroupInviteTiles()}
|
label={_t('Community Invites')}
|
||||||
label={_t('Community Invites')}
|
editable={false}
|
||||||
editable={false}
|
order="recent"
|
||||||
order="recent"
|
isInvite={true}
|
||||||
isInvite={true}
|
collapsed={self.props.collapsed}
|
||||||
collapsed={self.props.collapsed}
|
searchFilter={self.props.searchFilter}
|
||||||
searchFilter={self.props.searchFilter}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onShowMoreRooms={self.onShowMoreRooms}
|
||||||
onShowMoreRooms={self.onShowMoreRooms}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||||
label={_t('Invites')}
|
label={_t('Invites')}
|
||||||
editable={false}
|
editable={false}
|
||||||
order="recent"
|
order="recent"
|
||||||
isInvite={true}
|
isInvite={true}
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms}
|
onShowMoreRooms={self.onShowMoreRooms}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['m.favourite']}
|
<RoomSubList list={self.state.lists['m.favourite']}
|
||||||
label={_t('Favourites')}
|
label={_t('Favourites')}
|
||||||
tagName="m.favourite"
|
tagName="m.favourite"
|
||||||
emptyContent={this._getEmptyContent('m.favourite')}
|
emptyContent={this._getEmptyContent('m.favourite')}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="manual"
|
order="manual"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />
|
onShowMoreRooms={self.onShowMoreRooms} />
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||||
label={_t('People')}
|
label={_t('People')}
|
||||||
tagName="im.vector.fake.direct"
|
tagName="im.vector.fake.direct"
|
||||||
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
|
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
|
||||||
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
headerItems={this._getHeaderItems('im.vector.fake.direct')}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
alwaysShowHeader={true}
|
alwaysShowHeader={true}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />
|
onShowMoreRooms={self.onShowMoreRooms} />
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||||
label={_t('Rooms')}
|
label={_t('Rooms')}
|
||||||
editable={true}
|
editable={true}
|
||||||
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
|
||||||
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
headerItems={this._getHeaderItems('im.vector.fake.recent')}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />
|
onShowMoreRooms={self.onShowMoreRooms} />
|
||||||
|
|
||||||
{ Object.keys(self.state.lists).map((tagName) => {
|
{ Object.keys(self.state.lists).map((tagName) => {
|
||||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||||
return <RoomSubList list={self.state.lists[tagName]}
|
return <RoomSubList list={self.state.lists[tagName]}
|
||||||
key={tagName}
|
key={tagName}
|
||||||
label={tagName}
|
label={tagName}
|
||||||
tagName={tagName}
|
tagName={tagName}
|
||||||
emptyContent={this._getEmptyContent(tagName)}
|
emptyContent={this._getEmptyContent(tagName)}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="manual"
|
order="manual"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||||
}
|
}
|
||||||
}) }
|
}) }
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['m.lowpriority']}
|
<RoomSubList list={self.state.lists['m.lowpriority']}
|
||||||
label={_t('Low priority')}
|
label={_t('Low priority')}
|
||||||
tagName="m.lowpriority"
|
tagName="m.lowpriority"
|
||||||
emptyContent={this._getEmptyContent('m.lowpriority')}
|
emptyContent={this._getEmptyContent('m.lowpriority')}
|
||||||
editable={true}
|
editable={true}
|
||||||
order="recent"
|
order="recent"
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onHeaderClick={self.onSubListHeaderClick}
|
onHeaderClick={self.onSubListHeaderClick}
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />
|
onShowMoreRooms={self.onShowMoreRooms} />
|
||||||
|
|
||||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||||
label={_t('Historical')}
|
label={_t('Historical')}
|
||||||
editable={false}
|
editable={false}
|
||||||
order="recent"
|
order="recent"
|
||||||
collapsed={self.props.collapsed}
|
collapsed={self.props.collapsed}
|
||||||
alwaysShowHeader={true}
|
alwaysShowHeader={true}
|
||||||
startAsHidden={true}
|
startAsHidden={true}
|
||||||
showSpinner={self.state.isLoadingLeftRooms}
|
showSpinner={self.state.isLoadingLeftRooms}
|
||||||
onHeaderClick= {self.onArchivedHeaderClick}
|
onHeaderClick= {self.onArchivedHeaderClick}
|
||||||
incomingCall={self.state.incomingCall}
|
incomingCall={self.state.incomingCall}
|
||||||
searchFilter={self.props.searchFilter}
|
searchFilter={self.props.searchFilter}
|
||||||
onShowMoreRooms={self.onShowMoreRooms} />
|
onShowMoreRooms={self.onShowMoreRooms} />
|
||||||
</div>
|
</div>
|
||||||
</GeminiScrollbar>
|
</GeminiScrollbar>
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,13 +29,21 @@ module.exports = React.createClass({
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
name: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const name = room.currentState.getStateEvents('m.room.name', '');
|
const name = room.currentState.getStateEvents('m.room.name', '');
|
||||||
const myId = MatrixClientPeg.get().credentials.userId;
|
const myId = MatrixClientPeg.get().credentials.userId;
|
||||||
const defaultName = room.getDefaultRoomName(myId);
|
const defaultName = room.getDefaultRoomName(myId);
|
||||||
|
|
||||||
this._initialName = name ? name.getContent().name : '';
|
this.setState({
|
||||||
|
name: name ? name.getContent().name : '',
|
||||||
|
});
|
||||||
|
|
||||||
this._placeholderName = _t("Unnamed Room");
|
this._placeholderName = _t("Unnamed Room");
|
||||||
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
|
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
|
||||||
|
@ -44,7 +52,13 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomName: function() {
|
getRoomName: function() {
|
||||||
return this.refs.editor.getValue();
|
return this.state.name;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onValueChanged: function(value, shouldSubmit) {
|
||||||
|
this.setState({
|
||||||
|
name: value,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
@ -57,7 +71,8 @@ module.exports = React.createClass({
|
||||||
placeholderClassName="mx_RoomHeader_placeholder"
|
placeholderClassName="mx_RoomHeader_placeholder"
|
||||||
placeholder={this._placeholderName}
|
placeholder={this._placeholderName}
|
||||||
blurToCancel={false}
|
blurToCancel={false}
|
||||||
initialValue={this._initialName}
|
initialValue={this.state.name}
|
||||||
|
onValueChanged={this._onValueChanged}
|
||||||
dir="auto" />
|
dir="auto" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,26 +28,41 @@ module.exports = React.createClass({
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
topic: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
const room = this.props.room;
|
const room = this.props.room;
|
||||||
const topic = room.currentState.getStateEvents('m.room.topic', '');
|
const topic = room.currentState.getStateEvents('m.room.topic', '');
|
||||||
this._initialTopic = topic ? topic.getContent().topic : '';
|
this.setState({
|
||||||
|
topic: topic ? topic.getContent().topic : '',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getTopic: function() {
|
getTopic: function() {
|
||||||
return this.refs.editor.getValue();
|
return this.state.topic;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onValueChanged: function(value) {
|
||||||
|
this.setState({
|
||||||
|
topic: value,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const EditableText = sdk.getComponent("elements.EditableText");
|
const EditableText = sdk.getComponent("elements.EditableText");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditableText ref="editor"
|
<EditableText
|
||||||
className="mx_RoomHeader_topic mx_RoomHeader_editable"
|
className="mx_RoomHeader_topic mx_RoomHeader_editable"
|
||||||
placeholderClassName="mx_RoomHeader_placeholder"
|
placeholderClassName="mx_RoomHeader_placeholder"
|
||||||
placeholder={_t("Add a topic")}
|
placeholder={_t("Add a topic")}
|
||||||
blurToCancel={false}
|
blurToCancel={false}
|
||||||
initialValue={this._initialTopic}
|
initialValue={this.state.topic}
|
||||||
|
onValueChanged={this._onValueChanged}
|
||||||
dir="auto" />
|
dir="auto" />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
251
src/stores/RoomListStore.js
Normal file
251
src/stores/RoomListStore.js
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
/*
|
||||||
|
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 = {
|
||||||
|
"m.favourite": "manual",
|
||||||
|
"im.vector.fake.invite": "recent",
|
||||||
|
"im.vector.fake.recent": "recent",
|
||||||
|
"im.vector.fake.direct": "recent",
|
||||||
|
"m.lowpriority": "recent",
|
||||||
|
"im.vector.fake.archived": "recent",
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(lists).forEach((listKey) => {
|
||||||
|
let comparator;
|
||||||
|
switch (listOrders[listKey]) {
|
||||||
|
case "recent":
|
||||||
|
comparator = this._recentsComparator;
|
||||||
|
break;
|
||||||
|
case "manual":
|
||||||
|
default:
|
||||||
|
comparator = this._getManualComparator(listKey, optimisticRequest);
|
||||||
|
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;
|
|
@ -55,6 +55,7 @@ class TagOrderStore extends Store {
|
||||||
const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
|
const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
|
||||||
this._setState({
|
this._setState({
|
||||||
orderedTagsAccountData: tagOrderingEventContent.tags || null,
|
orderedTagsAccountData: tagOrderingEventContent.tags || null,
|
||||||
|
removedTagsAccountData: tagOrderingEventContent.removedTags || null,
|
||||||
hasSynced: true,
|
hasSynced: true,
|
||||||
});
|
});
|
||||||
this._updateOrderedTags();
|
this._updateOrderedTags();
|
||||||
|
@ -70,6 +71,7 @@ class TagOrderStore extends Store {
|
||||||
|
|
||||||
this._setState({
|
this._setState({
|
||||||
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
|
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
|
||||||
|
removedTagsAccountData: payload.event_content ? payload.event_content.removedTags : null,
|
||||||
});
|
});
|
||||||
this._updateOrderedTags();
|
this._updateOrderedTags();
|
||||||
break;
|
break;
|
||||||
|
@ -87,9 +89,18 @@ class TagOrderStore extends Store {
|
||||||
// Optimistic update of a moved tag
|
// Optimistic update of a moved tag
|
||||||
this._setState({
|
this._setState({
|
||||||
orderedTags: payload.request.tags,
|
orderedTags: payload.request.tags,
|
||||||
|
removedTagsAccountData: payload.request.removedTags,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'TagOrderActions.removeTag.pending': {
|
||||||
|
// Optimistic update of a removed tag
|
||||||
|
this._setState({
|
||||||
|
removedTagsAccountData: payload.request.removedTags,
|
||||||
|
});
|
||||||
|
this._updateOrderedTags();
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'select_tag': {
|
case 'select_tag': {
|
||||||
let newTags = [];
|
let newTags = [];
|
||||||
// Shift-click semantics
|
// Shift-click semantics
|
||||||
|
@ -165,13 +176,15 @@ class TagOrderStore extends Store {
|
||||||
_mergeGroupsAndTags() {
|
_mergeGroupsAndTags() {
|
||||||
const groupIds = this._state.joinedGroupIds || [];
|
const groupIds = this._state.joinedGroupIds || [];
|
||||||
const tags = this._state.orderedTagsAccountData || [];
|
const tags = this._state.orderedTagsAccountData || [];
|
||||||
|
const removedTags = new Set(this._state.removedTagsAccountData || []);
|
||||||
|
|
||||||
|
|
||||||
const tagsToKeep = tags.filter(
|
const tagsToKeep = tags.filter(
|
||||||
(t) => t[0] !== '+' || groupIds.includes(t),
|
(t) => (t[0] !== '+' || groupIds.includes(t)) && !removedTags.has(t),
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupIdsToAdd = groupIds.filter(
|
const groupIdsToAdd = groupIds.filter(
|
||||||
(groupId) => !tags.includes(groupId),
|
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return tagsToKeep.concat(groupIdsToAdd);
|
return tagsToKeep.concat(groupIdsToAdd);
|
||||||
|
@ -181,6 +194,10 @@ class TagOrderStore extends Store {
|
||||||
return this._state.orderedTags;
|
return this._state.orderedTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRemovedTagsAccountData() {
|
||||||
|
return this._state.removedTagsAccountData;
|
||||||
|
}
|
||||||
|
|
||||||
getStoreId() {
|
getStoreId() {
|
||||||
// Generate a random ID to prevent this store from clobbering its
|
// Generate a random ID to prevent this store from clobbering its
|
||||||
// state with redundant remote echos.
|
// state with redundant remote echos.
|
||||||
|
|
Loading…
Reference in a new issue