Merge remote-tracking branch 'origin/develop' into develop

pull/21833/head
Weblate 2018-01-19 14:49:55 +00:00
commit aff1fcde90
5 changed files with 218 additions and 139 deletions

View File

@ -78,8 +78,6 @@
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-beautiful-dnd": "^4.0.0", "react-beautiful-dnd": "^4.0.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.14.1", "sanitize-html": "^1.14.1",

View File

@ -19,8 +19,6 @@ 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-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
@ -347,4 +345,4 @@ const LoggedInView = React.createClass({
}, },
}); });
export default DragDropContext(HTML5Backend)(LoggedInView); export default LoggedInView;

View File

@ -1066,10 +1066,10 @@ export default React.createClass({
// this if we are not scrolled up in the view. To find out, delegate to // this if we are not scrolled up in the view. To find out, delegate to
// the timeline panel. If the timeline panel doesn't exist, then we assume // the timeline panel. If the timeline panel doesn't exist, then we assume
// it is safe to reset the timeline. // it is safe to reset the timeline.
if (!self._loggedInView) { if (!self._loggedInView || !self._loggedInView.child) {
return true; return true;
} }
return self._loggedInView.getDecoratedComponentInstance().canResetTimelineInRoom(roomId); return self._loggedInView.child.canResetTimelineInRoom(roomId);
}); });
cli.on('sync', function(state, prevState) { cli.on('sync', function(state, prevState) {

View File

@ -18,6 +18,7 @@ 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');
@ -32,6 +33,8 @@ const Receipt = require('../../../utils/Receipt');
import TagOrderStore from '../../../stores/TagOrderStore'; import TagOrderStore from '../../../stores/TagOrderStore';
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) {
@ -275,6 +278,103 @@ 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 = Object.assign({}, 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 prevOrder = newIndex === 0 ?
0 : newList[offset + newIndex - 1].tags[newTag].order;
const nextOrder = newIndex === newList.length ?
1 : newList[offset + newIndex].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),
@ -649,114 +749,116 @@ module.exports = React.createClass({
const self = this; const self = this;
return ( return (
<GeminiScrollbar className="mx_RoomList_scrollbar" <DragDropContext onDragEnd={this.onRoomTileEndDrag}>
autoshow={true} onScroll={self._whenScrolling} ref="gemscroll"> <GeminiScrollbar className="mx_RoomList_scrollbar"
<div className="mx_RoomList"> autoshow={true} onScroll={self._whenScrolling} ref="gemscroll">
<RoomSubList list={[]} <div className="mx_RoomList">
extraTiles={this._makeGroupInviteTiles()} <RoomSubList list={[]}
label={_t('Community Invites')} extraTiles={this._makeGroupInviteTiles()}
editable={false} label={_t('Community Invites')}
order="recent" editable={false}
isInvite={true} order="recent"
collapsed={self.props.collapsed} isInvite={true}
searchFilter={self.props.searchFilter} collapsed={self.props.collapsed}
onHeaderClick={self.onSubListHeaderClick} searchFilter={self.props.searchFilter}
onShowMoreRooms={self.onShowMoreRooms} onHeaderClick={self.onSubListHeaderClick}
/> 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>
); );
}, },
}); });

View File

@ -35,10 +35,7 @@ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
propTypes: { propTypes: {
connectDragSource: PropTypes.func,
connectDropTarget: PropTypes.func,
onClick: PropTypes.func, onClick: PropTypes.func,
isDragging: PropTypes.bool,
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
collapsed: PropTypes.bool.isRequired, collapsed: PropTypes.bool.isRequired,
@ -256,35 +253,19 @@ module.exports = React.createClass({
directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />; directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
} }
// These props are injected by React DnD, return <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
// as defined by your `collect` function above: <div className={avatarClasses}>
const isDragging = this.props.isDragging; <div className="mx_RoomTile_avatar_container">
const connectDragSource = this.props.connectDragSource; <RoomAvatar room={this.props.room} width={24} height={24} />
const connectDropTarget = this.props.connectDropTarget; { directMessageIndicator }
let ret = (
<div> { /* Only native elements can be wrapped in a DnD object. */ }
<AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ directMessageIndicator }
</div>
</div> </div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
</div> </div>
); <div className="mx_RoomTile_nameContainer">
{ label }
if (connectDropTarget) ret = connectDropTarget(ret); { badge }
if (connectDragSource) ret = connectDragSource(ret); </div>
{ /* { incomingCallBox } */ }
return ret; { tooltip }
</AccessibleButton>;
}, },
}); });