Merge pull request #369 from matrix-org/wmwragg/mention-state-menu

Wmwragg/mention state menu
This commit is contained in:
Matthew Hodgson 2016-08-03 15:22:39 +01:00 committed by GitHub
commit f95a11a9bf
7 changed files with 158 additions and 51 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -25,6 +25,7 @@ limitations under the License.
*/ */
module.exports.components = {}; module.exports.components = {};
module.exports.components['structures.ContextualMenu'] = require('./components/structures/ContextualMenu');
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var classNames = require('classnames');
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
@ -27,6 +28,12 @@ var ReactDOM = require('react-dom');
module.exports = { module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container", ContextualMenuContainerId: "mx_ContextualMenu_Container",
propTypes: {
menuWidth: React.PropTypes.number,
menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number,
},
getOrCreateContainer: function() { getOrCreateContainer: function() {
var container = document.getElementById(this.ContextualMenuContainerId); var container = document.getElementById(this.ContextualMenuContainerId);
@ -45,29 +52,50 @@ module.exports = {
var closeMenu = function() { var closeMenu = function() {
ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(self.getOrCreateContainer());
if (props && props.onFinished) props.onFinished.apply(null, arguments); if (props && props.onFinished) {
props.onFinished.apply(null, arguments);
}
}; };
var position = { var position = {
top: props.top - 20, top: props.top,
}; };
var chevronOffset = {
top: props.chevronOffset,
}
var chevron = null; var chevron = null;
if (props.left) { if (props.left) {
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" /> chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>
position.left = props.left + 8; position.left = props.left;
} else { } else {
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" /> chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>
position.right = props.right + 8; position.right = props.right;
} }
var className = 'mx_ContextualMenu_wrapper'; var className = 'mx_ContextualMenu_wrapper';
var menuClasses = classNames({
'mx_ContextualMenu': true,
'mx_ContextualMenu_left': props.left,
'mx_ContextualMenu_right': !props.left,
});
var menuSize = {};
if (props.menuWidth) {
menuSize.width = props.menuWidth;
}
if (props.menuHeight) {
menuSize.height = props.menuHeight;
}
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
var menu = ( var menu = (
<div className={className}> <div className={className} style={position}>
<div className="mx_ContextualMenu" style={position}> <div className={menuClasses} style={menuSize}>
{chevron} {chevron}
<Element {...props} onFinished={closeMenu}/> <Element {...props} onFinished={closeMenu}/>
</div> </div>

View File

@ -20,7 +20,7 @@ var Favico = require('favico.js');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
var Notifier = require("../../Notifier"); var Notifier = require("../../Notifier");
var ContextualMenu = require("../../ContextualMenu"); var ContextualMenu = require("./ContextualMenu");
var RoomListSorter = require("../../RoomListSorter"); var RoomListSorter = require("../../RoomListSorter");
var UserActivity = require("../../UserActivity"); var UserActivity = require("../../UserActivity");
var Presence = require("../../Presence"); var Presence = require("../../Presence");

View File

@ -23,7 +23,7 @@ var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg') var MatrixClientPeg = require('../../../MatrixClientPeg')
var TextForEvent = require('../../../TextForEvent'); var TextForEvent = require('../../../TextForEvent');
var ContextualMenu = require('../../../ContextualMenu'); var ContextualMenu = require('../../structures/ContextualMenu');
var dispatcher = require("../../../dispatcher"); var dispatcher = require("../../../dispatcher");
var ObjectUtils = require('../../../ObjectUtils'); var ObjectUtils = require('../../../ObjectUtils');
@ -249,12 +249,15 @@ module.exports = React.createClass({
}, },
onEditClicked: function(e) { onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu'); var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect() var buttonRect = e.target.getBoundingClientRect()
var x = buttonRect.right;
var y = buttonRect.top + (e.target.height / 2); // The window X and Y offsets are to adjust position when zoomed in to page
var x = buttonRect.right + window.pageXOffset;
var y = (buttonRect.top + (e.target.height / 2) + window.pageYOffset) - 19;
var self = this; var self = this;
ContextualMenu.createMenu(MessageContextMenu, { ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10,
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
left: x, left: x,
top: y, top: y,

View File

@ -268,9 +268,11 @@ module.exports = React.createClass({
}, },
_repositionTooltip: function(e) { _repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) { // We access the parent of the parent, as the tooltip is inside a container
// Needs refactoring into a better multipurpose tooltip
if (this.tooltip && this.tooltip.parentElement && this.tooltip.parentElement.parentElement) {
var scroll = ReactDOM.findDOMNode(this); var scroll = ReactDOM.findDOMNode(this);
this.tooltip.style.top = (70 + scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px"; this.tooltip.style.top = (3 + scroll.parentElement.offsetTop + this.tooltip.parentElement.parentElement.offsetTop - this._getScrollNode().scrollTop) + "px";
} }
}, },

View File

@ -21,6 +21,7 @@ var classNames = require('classnames');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index'); var sdk = require('../../../index');
var ContextualMenu = require('../../structures/ContextualMenu');
import {emojifyText} from '../../../HtmlUtils'; import {emojifyText} from '../../../HtmlUtils';
module.exports = React.createClass({ module.exports = React.createClass({
@ -43,16 +44,48 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
var areNotifsMuted = false;
var cli = MatrixClientPeg.get();
if (!cli.isGuest()) {
var roomPushRule = cli.getRoomPushRule("global", this.props.room.roomId);
if (roomPushRule) {
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
areNotifsMuted = true;
}
}
}
return({ return({
hover : false, hover : false,
badgeHover : false, badgeHover : false,
menu: false,
areNotifsMuted: areNotifsMuted,
}); });
}, },
onAction: function(payload) {
switch (payload.action) {
case 'notification_change':
// Is the notification about this room?
if (payload.roomId === this.props.room.roomId) {
this.setState( { areNotifsMuted : payload.isMuted });
}
break;
}
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onClick: function() { onClick: function() {
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: this.props.room.roomId room_id: this.props.room.roomId,
}); });
}, },
@ -65,13 +98,47 @@ module.exports = React.createClass({
}, },
badgeOnMouseEnter: function() { badgeOnMouseEnter: function() {
this.setState( { badgeHover : true } ); // Only allow none guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover : true } );
}
}, },
badgeOnMouseLeave: function() { badgeOnMouseLeave: function() {
this.setState( { badgeHover : false } ); this.setState( { badgeHover : false } );
}, },
onBadgeClicked: function(e) {
// Only allow none guests to access the context menu
if (!MatrixClientPeg.get().isGuest()) {
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
this.setState({ hover: false });
}
var Menu = sdk.getComponent('context_menus.NotificationStateContextMenu');
var elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
var x = elementRect.right + window.pageXOffset + 3;
var y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset) - 53;
var self = this;
ContextualMenu.createMenu(Menu, {
menuWidth: 188,
menuHeight: 126,
chevronOffset: 45,
left: x,
top: y,
room: this.props.room,
onFinished: function() {
self.setState({ menu: false });
}
});
this.setState({ menu: true });
}
},
render: function() { render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId; var myUserId = MatrixClientPeg.get().credentials.userId;
var me = this.props.room.currentState.members[myUserId]; var me = this.props.room.currentState.members[myUserId];
@ -84,60 +151,63 @@ module.exports = React.createClass({
'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notificationCount > 0, 'mx_RoomTile_unreadNotify': notificationCount > 0,
'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0),
'mx_RoomTile_highlight': this.props.highlight, 'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': (me && me.membership == 'invite'), 'mx_RoomTile_invited': (me && me.membership == 'invite'),
'mx_RoomTile_menu': this.state.menu,
});
var avatarClasses = classNames({
'mx_RoomTile_avatar': true,
'mx_RoomTile_mute': this.state.areNotifsMuted,
});
var badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || this.state.menu,
'mx_RoomTile_badgeMute': this.state.areNotifsMuted,
}); });
// XXX: We should never display raw room IDs, but sometimes the // XXX: We should never display raw room IDs, but sometimes the
// room name js sdk gives is undefined (cannot repro this -- k) // room name js sdk gives is undefined (cannot repro this -- k)
var name = this.props.room.name || this.props.room.roomId; var name = this.props.room.name || this.props.room.roomId;
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge; var badge;
var badgeContent; var badgeContent;
var badgeClasses;
if (this.state.badgeHover) { if (this.state.badgeHover || this.state.menu) {
badgeContent = "\u00B7\u00B7\u00B7"; badgeContent = "\u00B7\u00B7\u00B7";
} else if (this.props.highlight || notificationCount > 0) { } else if (this.props.highlight || notificationCount > 0) {
badgeContent = notificationCount ? notificationCount : '!'; var limitedCount = (notificationCount > 99) ? '99+' : notificationCount;
badgeContent = notificationCount ? limitedCount : '!';
} else { } else {
badgeContent = '\u200B'; badgeContent = '\u200B';
} }
if (this.props.highlight || notificationCount > 0) { if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) {
badgeClasses = "mx_RoomTile_badge"; badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}><img className="mx_RoomTile_badgeIcon" src="img/icon-context-mute.svg" width="16" height="12" /></div>;
} else { } else {
badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; badge = <div className={ badgeClasses } onClick={this.onBadgeClicked} onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
} }
badge = <div className={ badgeClasses } onMouseEnter={this.badgeOnMouseEnter} onMouseLeave={this.badgeOnMouseLeave}>{ badgeContent }</div>;
/*
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge">!</div>;
}
else if (this.props.unread) {
badge = <div className="mx_RoomTile_badge">1</div>;
}
var nameCell;
if (badge) {
nameCell = <div className="mx_RoomTile_nameBadge"><div className="mx_RoomTile_name">{name}</div><div className="mx_RoomTile_badgeCell">{badge}</div></div>;
}
else {
nameCell = <div className="mx_RoomTile_name">{name}</div>;
}
*/
var label; var label;
var tooltip;
if (!this.props.collapsed) { if (!this.props.collapsed) {
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); var nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_mute': this.state.areNotifsMuted,
'mx_RoomTile_badgeShown': this.props.highlight || notificationCount > 0 || this.state.badgeHover || this.state.menu || this.state.areNotifsMuted,
});
let nameHTML = emojifyText(name); let nameHTML = emojifyText(name);
if (this.props.selected) { if (this.props.selected) {
name = <span dangerouslySetInnerHTML={nameHTML}></span>; let nameSelected = <span dangerouslySetInnerHTML={nameHTML}></span>;
label = <div className={ className }>{ name }</div>;
label = <div title={ name } onClick={this.onClick} className={ nameClasses }>{ nameSelected }</div>;
} else { } else {
label = <div className={ className } dangerouslySetInnerHTML={nameHTML}></div>; label = <div title={ name } onClick={this.onClick} className={ nameClasses } dangerouslySetInnerHTML={nameHTML}></div>;
} }
} }
else if (this.state.hover) { else if (this.state.hover) {
@ -160,13 +230,16 @@ module.exports = React.createClass({
var connectDropTarget = this.props.connectDropTarget; var connectDropTarget = this.props.connectDropTarget;
return connectDragSource(connectDropTarget( return connectDragSource(connectDropTarget(
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <div className={classes} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar"> <div className={avatarClasses}>
<RoomAvatar room={this.props.room} width={24} height={24} /> <RoomAvatar onClick={this.onClick} room={this.props.room} width={24} height={24} />
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div> </div>
{ label }
{ badge }
{ incomingCallBox } { incomingCallBox }
{ tooltip }
</div> </div>
)); ));
} }