diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000..5008ddfcf5 Binary files /dev/null and b/.DS_Store differ diff --git a/src/component-index.js b/src/component-index.js index 19a016aec8..ca8887858c 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -25,6 +25,7 @@ limitations under the License. */ 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.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); diff --git a/src/ContextualMenu.js b/src/components/structures/ContextualMenu.js similarity index 63% rename from src/ContextualMenu.js rename to src/components/structures/ContextualMenu.js index e720b69eda..fcfc5d5e50 100644 --- a/src/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; +var classNames = require('classnames'); var React = require('react'); var ReactDOM = require('react-dom'); @@ -27,6 +28,12 @@ var ReactDOM = require('react-dom'); module.exports = { ContextualMenuContainerId: "mx_ContextualMenu_Container", + propTypes: { + menuWidth: React.PropTypes.number, + menuHeight: React.PropTypes.number, + chevronOffset: React.PropTypes.number, + }, + getOrCreateContainer: function() { var container = document.getElementById(this.ContextualMenuContainerId); @@ -45,29 +52,50 @@ module.exports = { var closeMenu = function() { ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); - if (props && props.onFinished) props.onFinished.apply(null, arguments); + if (props && props.onFinished) { + props.onFinished.apply(null, arguments); + } }; var position = { - top: props.top - 20, + top: props.top, }; + var chevronOffset = { + top: props.chevronOffset, + } + var chevron = null; if (props.left) { - chevron = - position.left = props.left + 8; + chevron =
+ position.left = props.left; } else { - chevron = - position.right = props.right + 8; + chevron =
+ position.right = props.right; } 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 // property set here so you can't close the menu from a button click! var menu = ( -
-
+
+
{chevron}
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dc9ca08e94..f1d69cada9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -20,7 +20,7 @@ var Favico = require('favico.js'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Notifier = require("../../Notifier"); -var ContextualMenu = require("../../ContextualMenu"); +var ContextualMenu = require("./ContextualMenu"); var RoomListSorter = require("../../RoomListSorter"); var UserActivity = require("../../UserActivity"); var Presence = require("../../Presence"); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 77be8226a2..7945debd1a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -23,7 +23,7 @@ var sdk = require('../../../index'); var MatrixClientPeg = require('../../../MatrixClientPeg') var TextForEvent = require('../../../TextForEvent'); -var ContextualMenu = require('../../../ContextualMenu'); +var ContextualMenu = require('../../structures/ContextualMenu'); var dispatcher = require("../../../dispatcher"); var ObjectUtils = require('../../../ObjectUtils'); @@ -249,12 +249,15 @@ module.exports = React.createClass({ }, onEditClicked: function(e) { - var MessageContextMenu = sdk.getComponent('rooms.MessageContextMenu'); + var MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu'); 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; ContextualMenu.createMenu(MessageContextMenu, { + chevronOffset: 10, mxEvent: this.props.mxEvent, left: x, top: y, diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 0c8ac7ed8d..8e57ceab9b 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -268,9 +268,11 @@ module.exports = React.createClass({ }, _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); - 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"; } }, diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index aa83110632..602ed4ee04 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -21,6 +21,7 @@ var classNames = require('classnames'); var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); var sdk = require('../../../index'); +var ContextualMenu = require('../../structures/ContextualMenu'); import {emojifyText} from '../../../HtmlUtils'; module.exports = React.createClass({ @@ -43,16 +44,48 @@ module.exports = React.createClass({ }, 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({ hover : 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() { dis.dispatch({ 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() { - 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() { 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() { var myUserId = MatrixClientPeg.get().credentials.userId; var me = this.props.room.currentState.members[myUserId]; @@ -84,60 +151,63 @@ module.exports = React.createClass({ 'mx_RoomTile_selected': this.props.selected, 'mx_RoomTile_unread': this.props.unread, 'mx_RoomTile_unreadNotify': notificationCount > 0, + 'mx_RoomTile_read': !(this.props.highlight || notificationCount > 0), 'mx_RoomTile_highlight': this.props.highlight, '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 // room name js sdk gives is undefined (cannot repro this -- k) 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 + var badge; var badgeContent; - var badgeClasses; - if (this.state.badgeHover) { + if (this.state.badgeHover || this.state.menu) { badgeContent = "\u00B7\u00B7\u00B7"; } else if (this.props.highlight || notificationCount > 0) { - badgeContent = notificationCount ? notificationCount : '!'; + var limitedCount = (notificationCount > 99) ? '99+' : notificationCount; + badgeContent = notificationCount ? limitedCount : '!'; } else { badgeContent = '\u200B'; } - if (this.props.highlight || notificationCount > 0) { - badgeClasses = "mx_RoomTile_badge"; + if (this.state.areNotifsMuted && !(this.state.badgeHover || this.state.menu)) { + badge =
; } else { - badgeClasses = "mx_RoomTile_badge mx_RoomTile_badge_no_unread"; + badge =
{ badgeContent }
; } - badge =
{ badgeContent }
; - - /* - if (this.props.highlight) { - badge =
!
; - } - else if (this.props.unread) { - badge =
1
; - } - var nameCell; - if (badge) { - nameCell =
{name}
{badge}
; - } - else { - nameCell =
{name}
; - } - */ - var label; + var tooltip; 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); if (this.props.selected) { - name = ; - label =
{ name }
; + let nameSelected = ; + + label =
{ nameSelected }
; } else { - label =
; + label =
; } } else if (this.state.hover) { @@ -160,13 +230,16 @@ module.exports = React.createClass({ var connectDropTarget = this.props.connectDropTarget; return connectDragSource(connectDropTarget( -
-
- +
+
+ +
+
+ { label } + { badge }
- { label } - { badge } { incomingCallBox } + { tooltip }
)); }