diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js new file mode 100644 index 0000000000..5adde400a6 --- /dev/null +++ b/src/components/views/rooms/MemberTile.js @@ -0,0 +1,215 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); + +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require('../../../index'); +var dis = require('../../../dispatcher'); +var Modal = require("../../../Modal"); + +// The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes +// on rendering them. Revert to Arial when this happens, which on OSX works at least. +var zalgo = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/; + +module.exports = React.createClass({ + displayName: 'MemberTile', + + getInitialState: function() { + return {}; + }, + + onLeaveClick: function() { + var QuestionDialog = sdk.getComponent("organisms.QuestionDialog"); + + var roomId = this.props.member.roomId; + Modal.createDialog(QuestionDialog, { + title: "Leave room", + description: "Are you sure you want to leave the room?", + onFinished: function(should_leave) { + if (should_leave) { + var d = MatrixClientPeg.get().leave(roomId); + + // FIXME: controller shouldn't be loading a view :( + var Loader = sdk.getComponent("elements.Spinner"); + var modal = Modal.createDialog(Loader); + + d.then(function() { + modal.close(); + dis.dispatch({action: 'view_next_room'}); + }, function(err) { + modal.close(); + Modal.createDialog(ErrorDialog, { + title: "Failed to leave room", + description: err.toString() + }); + }); + } + } + }); + }, + + shouldComponentUpdate: function(nextProps, nextState) { + if (this.state.hover !== nextState.hover) return true; + if ( + this.member_last_modified_time === undefined || + this.member_last_modified_time < nextProps.member.getLastModifiedTime() + ) { + return true + } + if ( + nextProps.member.user && + (this.user_last_modified_time === undefined || + this.user_last_modified_time < nextProps.member.user.getLastModifiedTime()) + ) { + return true + } + return false; + }, + + mouseEnter: function(e) { + this.setState({ 'hover': true }); + }, + + mouseLeave: function(e) { + this.setState({ 'hover': false }); + }, + + onClick: function(e) { + dis.dispatch({ + action: 'view_user', + member: this.props.member, + }); + }, + + getDuration: function(time) { + if (!time) return; + var t = parseInt(time / 1000); + var s = t % 60; + var m = parseInt(t / 60) % 60; + var h = parseInt(t / (60 * 60)) % 24; + var d = parseInt(t / (60 * 60 * 24)); + if (t < 60) { + if (t < 0) { + return "0s"; + } + return s + "s"; + } + if (t < 60 * 60) { + return m + "m"; + } + if (t < 24 * 60 * 60) { + return h + "h"; + } + return d + "d "; + }, + + getPrettyPresence: function(user) { + if (!user) return "Unknown"; + var presence = user.presence; + if (presence === "online") return "Online"; + if (presence === "unavailable") return "Idle"; // XXX: is this actually right? + if (presence === "offline") return "Offline"; + return "Unknown"; + }, + + getPowerLabel: function() { + var label = this.props.member.userId; + if (this.state.isTargetMod) { + label += " - Mod (" + this.props.member.powerLevelNorm + "%)"; + } + return label; + }, + + render: function() { + this.member_last_modified_time = this.props.member.getLastModifiedTime(); + if (this.props.member.user) { + this.user_last_modified_time = this.props.member.user.getLastModifiedTime(); + } + + var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId; + + var power; + // if (this.props.member && this.props.member.powerLevelNorm > 0) { + // var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png"; + // power = ; + // } + var presenceClass = "mx_MemberTile_offline"; + var mainClassName = "mx_MemberTile "; + if (this.props.member.user) { + if (this.props.member.user.presence === "online") { + presenceClass = "mx_MemberTile_online"; + } + else if (this.props.member.user.presence === "unavailable") { + presenceClass = "mx_MemberTile_unavailable"; + } + } + mainClassName += presenceClass; + if (this.state.hover) { + mainClassName += " mx_MemberTile_hover"; + } + + var name = this.props.member.name; + // if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain + //var leave = isMyUser ? : null; + + var nameClass = "mx_MemberTile_name"; + if (zalgo.test(name)) { + nameClass += " mx_MemberTile_zalgo"; + } + + var nameEl; + if (this.state.hover) { + var presence; + // FIXME: make presence data update whenever User.presence changes... + var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1; + if (active >= 0) { + presence =
{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago
; + } + else { + presence =
{ this.getPrettyPresence(this.props.member.user) }
; + } + + nameEl = +
+ +
{ name }
+ { presence } +
+ } + else { + nameEl = +
+ { name } +
+ } + + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + return ( +
+
+ + { power } +
+ { nameEl } +
+ ); + } +}); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js new file mode 100644 index 0000000000..b9233f2a32 --- /dev/null +++ b/src/components/views/rooms/RoomTile.js @@ -0,0 +1,142 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +'use strict'; + +var React = require('react'); +var classNames = require('classnames'); +var dis = require("../../../dispatcher"); +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require('../../../index'); + +module.exports = React.createClass({ + displayName: 'RoomTile', + + propTypes: { + // TODO: We should *optionally* support DND stuff and ideally be impl agnostic about it + connectDragSource: React.PropTypes.func.isRequired, + connectDropTarget: React.PropTypes.func.isRequired, + isDragging: React.PropTypes.bool.isRequired, + + room: React.PropTypes.object.isRequired, + collapsed: React.PropTypes.bool.isRequired, + selected: React.PropTypes.bool.isRequired, + unread: React.PropTypes.bool.isRequired, + highlight: React.PropTypes.bool.isRequired, + isInvite: React.PropTypes.bool.isRequired, + roomSubList: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return( { hover : false }); + }, + + onClick: function() { + dis.dispatch({ + action: 'view_room', + room_id: this.props.room.roomId + }); + }, + + onMouseEnter: function() { + this.setState( { hover : true }); + }, + + onMouseLeave: function() { + this.setState( { hover : false }); + }, + + render: function() { + // if (this.props.clientOffset) { + // //console.log("room " + this.props.room.roomId + " has dropTarget clientOffset " + this.props.clientOffset.x + "," + this.props.clientOffset.y); + // } + +/* + if (this.props.room._dragging) { + var RoomDropTarget = sdk.getComponent("molecules.RoomDropTarget"); + return ; + } +*/ + + var myUserId = MatrixClientPeg.get().credentials.userId; + var me = this.props.room.currentState.members[myUserId]; + var classes = classNames({ + 'mx_RoomTile': true, + 'mx_RoomTile_selected': this.props.selected, + 'mx_RoomTile_unread': this.props.unread, + 'mx_RoomTile_highlight': this.props.highlight, + 'mx_RoomTile_invited': (me && me.membership == 'invite'), + }); + + var name; + if (this.props.isInvite) { + name = this.props.room.getMember(myUserId).events.member.getSender(); + } + else { + // XXX: We should never display raw room IDs, but sometimes the room name js sdk gives is undefined + 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; + if (this.props.highlight) { + badge =
; + } + /* + if (this.props.highlight) { + badge =
!
; + } + else if (this.props.unread) { + badge =
1
; + } + var nameCell; + if (badge) { + nameCell =
{name}
{badge}
; + } + else { + nameCell =
{name}
; + } + */ + + var label; + if (!this.props.collapsed) { + var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : ''); + label =
{name}
; + } + else if (this.state.hover) { + var RoomTooltip = sdk.getComponent("molecules.RoomTooltip"); + label = ; + } + + var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + + // These props are injected by React DnD, + // as defined by your `collect` function above: + var isDragging = this.props.isDragging; + var connectDragSource = this.props.connectDragSource; + var connectDropTarget = this.props.connectDropTarget; + + return connectDragSource(connectDropTarget( +
+
+ + { badge } +
+ { label } +
+ )); + } +}); diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js deleted file mode 100644 index 057bc82497..0000000000 --- a/src/controllers/molecules/MemberTile.js +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var dis = require("../../dispatcher"); -var Modal = require("../../Modal"); -var sdk = require('../../index.js'); - -var MatrixClientPeg = require("../../MatrixClientPeg"); - -module.exports = { - getInitialState: function() { - return {}; - }, - - onLeaveClick: function() { - var QuestionDialog = sdk.getComponent("organisms.QuestionDialog"); - - var roomId = this.props.member.roomId; - Modal.createDialog(QuestionDialog, { - title: "Leave room", - description: "Are you sure you want to leave the room?", - onFinished: function(should_leave) { - if (should_leave) { - var d = MatrixClientPeg.get().leave(roomId); - - // FIXME: controller shouldn't be loading a view :( - var Loader = sdk.getComponent("elements.Spinner"); - var modal = Modal.createDialog(Loader); - - d.then(function() { - modal.close(); - dis.dispatch({action: 'view_next_room'}); - }, function(err) { - modal.close(); - Modal.createDialog(ErrorDialog, { - title: "Failed to leave room", - description: err.toString() - }); - }); - } - } - }); - } -}; diff --git a/src/controllers/molecules/RoomTile.js b/src/controllers/molecules/RoomTile.js deleted file mode 100644 index 78927ec55e..0000000000 --- a/src/controllers/molecules/RoomTile.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2015 OpenMarket 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. -*/ - -'use strict'; - -var dis = require("../../dispatcher"); - -module.exports = { - onClick: function() { - dis.dispatch({ - action: 'view_room', - room_id: this.props.room.roomId - }); - }, -};