diff --git a/src/CallHandler.js b/src/CallHandler.js new file mode 100644 index 0000000000..b56137dee9 --- /dev/null +++ b/src/CallHandler.js @@ -0,0 +1,230 @@ +/* +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. +*/ + +/* + * Manages a list of all the currently active calls. + * + * This handler dispatches when voip calls are added/updated/removed from this list: + * { + * action: 'call_state' + * room_id: + * } + * + * To know the state of the call, this handler exposes a getter to + * obtain the call for a room: + * var call = CallHandler.getCall(roomId) + * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + * + * This handler listens for and handles the following actions: + * { + * action: 'place_call', + * type: 'voice|video', + * room_id: + * } + * + * { + * action: 'incoming_call' + * call: MatrixCall + * } + * + * { + * action: 'hangup' + * room_id: + * } + * + * { + * action: 'answer' + * room_id: + * } + */ + +var MatrixClientPeg = require("./MatrixClientPeg"); +var Modal = require("./Modal"); +var sdk = require("./index"); +var Matrix = require("matrix-js-sdk"); +var dis = require("./dispatcher"); + +var calls = { + //room_id: MatrixCall +}; + +function play(audioId) { + // TODO: Attach an invisible element for this instead + // which listens? + var audio = document.getElementById(audioId); + if (audio) { + audio.load(); + audio.play(); + } +} + +function pause(audioId) { + // TODO: Attach an invisible element for this instead + // which listens? + var audio = document.getElementById(audioId); + if (audio) { + audio.pause(); + } +} + +function _setCallListeners(call) { + call.on("error", function(err) { + console.error("Call error: %s", err); + console.error(err.stack); + call.hangup(); + _setCallState(undefined, call.roomId, "ended"); + }); + call.on("hangup", function() { + _setCallState(undefined, call.roomId, "ended"); + }); + // map web rtc states to dummy UI state + // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing + call.on("state", function(newState, oldState) { + if (newState === "ringing") { + _setCallState(call, call.roomId, "ringing"); + pause("ringbackAudio"); + } + else if (newState === "invite_sent") { + _setCallState(call, call.roomId, "ringback"); + play("ringbackAudio"); + } + else if (newState === "ended" && oldState === "connected") { + _setCallState(call, call.roomId, "ended"); + pause("ringbackAudio"); + play("callendAudio"); + } + else if (newState === "ended" && oldState === "invite_sent" && + (call.hangupParty === "remote" || + (call.hangupParty === "local" && call.hangupReason === "invite_timeout") + )) { + _setCallState(call, call.roomId, "busy"); + pause("ringbackAudio"); + play("busyAudio"); + + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Call Timeout", + description: "The remote side failed to pick up." + }); + } + else if (oldState === "invite_sent") { + _setCallState(call, call.roomId, "stop_ringback"); + pause("ringbackAudio"); + } + else if (oldState === "ringing") { + _setCallState(call, call.roomId, "stop_ringing"); + pause("ringbackAudio"); + } + else if (newState === "connected") { + _setCallState(call, call.roomId, "connected"); + pause("ringbackAudio"); + } + }); +} + +function _setCallState(call, roomId, status) { + console.log( + "Call state in %s changed to %s (%s)", roomId, status, (call ? call.state : "-") + ); + calls[roomId] = call; + if (call) { + call.call_state = status; + } + dis.dispatch({ + action: 'call_state', + room_id: roomId + }); +} + +dis.register(function(payload) { + switch (payload.action) { + case 'place_call': + if (calls[payload.room_id]) { + return; // don't allow >1 call to be placed. + } + var room = MatrixClientPeg.get().getRoom(payload.room_id); + if (!room) { + console.error("Room %s does not exist.", payload.room_id); + return; + } + var members = room.getJoinedMembers(); + if (members.length !== 2) { + var text = members.length === 1 ? "yourself." : "more than 2 people."; + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + description: "You cannot place a call with " + text + }); + console.error( + "Fail: There are %s joined members in this room, not 2.", + room.getJoinedMembers().length + ); + return; + } + console.log("Place %s call in %s", payload.type, payload.room_id); + var call = Matrix.createNewMatrixCall( + MatrixClientPeg.get(), payload.room_id + ); + _setCallListeners(call); + _setCallState(call, call.roomId, "ringback"); + if (payload.type === 'voice') { + call.placeVoiceCall(); + } + else if (payload.type === 'video') { + call.placeVideoCall( + payload.remote_element, + payload.local_element + ); + } + else { + console.error("Unknown call type: %s", payload.type); + } + + break; + case 'incoming_call': + if (calls[payload.call.roomId]) { + payload.call.hangup("busy"); + return; // don't allow >1 call to be received, hangup newer one. + } + var call = payload.call; + _setCallListeners(call); + _setCallState(call, call.roomId, "ringing"); + break; + case 'hangup': + if (!calls[payload.room_id]) { + return; // no call to hangup + } + calls[payload.room_id].hangup(); + _setCallState(null, payload.room_id, "ended"); + break; + case 'answer': + if (!calls[payload.room_id]) { + return; // no call to answer + } + calls[payload.room_id].answer(); + _setCallState(calls[payload.room_id], payload.room_id, "connected"); + dis.dispatch({ + action: "view_room", + room_id: payload.room_id + }); + break; + } +}); + +module.exports = { + getCall: function(roomId) { + return calls[roomId] || null; + } +}; diff --git a/src/WhoIsTyping.js b/src/WhoIsTyping.js new file mode 100644 index 0000000000..4fb5399027 --- /dev/null +++ b/src/WhoIsTyping.js @@ -0,0 +1,49 @@ +var MatrixClientPeg = require("./MatrixClientPeg"); + +module.exports = { + usersTypingApartFromMe: function(room) { + return this.usersTyping( + room, [MatrixClientPeg.get().credentials.userId] + ); + }, + + /** + * Given a Room object and, optionally, a list of userID strings + * to exclude, return a list of user objects who are typing. + */ + usersTyping: function(room, exclude) { + var whoIsTyping = []; + + if (exclude === undefined) { + exclude = []; + } + + var memberKeys = Object.keys(room.currentState.members); + for (var i = 0; i < memberKeys.length; ++i) { + var userId = memberKeys[i]; + + if (room.currentState.members[userId].typing) { + if (exclude.indexOf(userId) == -1) { + whoIsTyping.push(room.currentState.members[userId]); + } + } + } + + return whoIsTyping; + }, + + whoIsTypingString: function(room) { + var whoIsTyping = this.usersTypingApartFromMe(room); + if (whoIsTyping.length == 0) { + return null; + } else if (whoIsTyping.length == 1) { + return whoIsTyping[0].name + ' is typing'; + } else { + var names = whoIsTyping.map(function(m) { + return m.name; + }); + var lastPerson = names.shift(); + return names.join(', ') + ' and ' + lastPerson + ' are typing'; + } + } +} diff --git a/src/controllers/atoms/EnableNotificationsButton.js b/src/controllers/atoms/EnableNotificationsButton.js index e116cd9671..3c399484e8 100644 --- a/src/controllers/atoms/EnableNotificationsButton.js +++ b/src/controllers/atoms/EnableNotificationsButton.js @@ -16,7 +16,6 @@ limitations under the License. 'use strict'; var sdk = require('../../index'); -var Notifier = sdk.getComponent('organisms/Notifier'); var dis = require("../../dispatcher"); module.exports = { @@ -37,10 +36,12 @@ module.exports = { }, enabled: function() { + var Notifier = sdk.getComponent('organisms.Notifier'); return Notifier.isEnabled(); }, onClick: function() { + var Notifier = sdk.getComponent('organisms.Notifier'); var self = this; if (!Notifier.supportsDesktopNotifications()) { return; diff --git a/src/controllers/atoms/MemberAvatar.js b/src/controllers/atoms/MemberAvatar.js new file mode 100644 index 0000000000..6ad08911b9 --- /dev/null +++ b/src/controllers/atoms/MemberAvatar.js @@ -0,0 +1,62 @@ +/* +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'); + +module.exports = { + propTypes: { + member: React.PropTypes.object.isRequired, + width: React.PropTypes.number, + height: React.PropTypes.number, + resizeMethod: React.PropTypes.string, + }, + + getDefaultProps: function() { + return { + width: 40, + height: 40, + resizeMethod: 'crop' + } + }, + + defaultAvatarUrl: function(member) { + return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAIAAAADnC86AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNrszQENADAIACB9QjNbxSKP4eagAFnTseHFErFYLBaLxWKxWCwWi8Vi8cX4CzAABSwCRWJw31gAAAAASUVORK5CYII="; + }, + + onError: function(ev) { + // don't tightloop if the browser can't load a data url + if (ev.target.src == this.defaultAvatarUrl(this.props.member)) { + return; + } + this.setState({ + imageUrl: this.defaultAvatarUrl(this.props.member) + }); + }, + + getInitialState: function() { + return { + imageUrl: MatrixClientPeg.get().getAvatarUrlForMember( + this.props.member, + this.props.width, + this.props.height, + this.props.resizeMethod + ) + }; + } +}; diff --git a/src/controllers/atoms/voip/VideoFeed.js b/src/controllers/atoms/voip/VideoFeed.js new file mode 100644 index 0000000000..3d34134daa --- /dev/null +++ b/src/controllers/atoms/voip/VideoFeed.js @@ -0,0 +1,19 @@ +/* +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. +*/ + +module.exports = { +}; + diff --git a/src/controllers/molecules/EventAsTextTile.js b/src/controllers/molecules/EventAsTextTile.js new file mode 100644 index 0000000000..3d34134daa --- /dev/null +++ b/src/controllers/molecules/EventAsTextTile.js @@ -0,0 +1,19 @@ +/* +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. +*/ + +module.exports = { +}; + diff --git a/src/controllers/molecules/MemberTile.js b/src/controllers/molecules/MemberTile.js index ad3ba5ea0e..a914fc6279 100644 --- a/src/controllers/molecules/MemberTile.js +++ b/src/controllers/molecules/MemberTile.js @@ -19,7 +19,6 @@ limitations under the License. var dis = require("../../dispatcher"); var Modal = require("../../Modal"); var sdk = require('../../index.js'); -var QuestionDialog = ComponentBroker.get("organisms/QuestionDialog"); var Loader = require("react-loader"); var MatrixClientPeg = require("../../MatrixClientPeg"); @@ -33,6 +32,8 @@ module.exports = { }, onLeaveClick: function() { + var QuestionDialog = sdk.getComponent("organisms.QuestionDialog"); + var roomId = this.props.member.roomId; Modal.createDialog(QuestionDialog, { title: "Leave room", diff --git a/src/controllers/molecules/RoomSettings.js b/src/controllers/molecules/RoomSettings.js new file mode 100644 index 0000000000..3c0682d09a --- /dev/null +++ b/src/controllers/molecules/RoomSettings.js @@ -0,0 +1,29 @@ +/* +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. +*/ + +var React = require('react'); + +module.exports = { + propTypes: { + room: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + power_levels_changed: false + }; + } +}; diff --git a/src/controllers/molecules/voip/CallView.js b/src/controllers/molecules/voip/CallView.js new file mode 100644 index 0000000000..c8fab1fba4 --- /dev/null +++ b/src/controllers/molecules/voip/CallView.js @@ -0,0 +1,70 @@ +/* +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. +*/ + +var dis = require("../../../dispatcher"); +var CallHandler = require("../../../CallHandler"); + +/* + * State vars: + * this.state.call = MatrixCall|null + * + * Props: + * this.props.room = Room (JS SDK) + */ + +module.exports = { + + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + if (this.props.room) { + this.showCall(this.props.room.roomId); + } + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + // if we were given a room_id to track, don't handle anything else. + if (payload.room_id && this.props.room && + this.props.room.roomId !== payload.room_id) { + return; + } + if (payload.action !== 'call_state') { + return; + } + this.showCall(payload.room_id); + }, + + showCall: function(roomId) { + var call = CallHandler.getCall(roomId); + if (call) { + call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); + // N.B. the remote video element is used for playback for audio for voice calls + call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); + } + if (call && call.type === "video" && call.state !== 'ended') { + this.getVideoView().getLocalVideoElement().style.display = "initial"; + this.getVideoView().getRemoteVideoElement().style.display = "initial"; + } + else { + this.getVideoView().getLocalVideoElement().style.display = "none"; + this.getVideoView().getRemoteVideoElement().style.display = "none"; + } + } +}; + diff --git a/src/controllers/molecules/voip/IncomingCallBox.js b/src/controllers/molecules/voip/IncomingCallBox.js new file mode 100644 index 0000000000..9ecced56c5 --- /dev/null +++ b/src/controllers/molecules/voip/IncomingCallBox.js @@ -0,0 +1,73 @@ +/* +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. +*/ + +var dis = require("../../../dispatcher"); +var CallHandler = require("../../../CallHandler"); + +module.exports = { + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + getInitialState: function() { + return { + incomingCall: null + } + }, + + onAction: function(payload) { + if (payload.action !== 'call_state') { + return; + } + var call = CallHandler.getCall(payload.room_id); + if (!call || call.call_state !== 'ringing') { + this.setState({ + incomingCall: null, + }); + this.getRingAudio().pause(); + return; + } + if (call.call_state === "ringing") { + this.getRingAudio().load(); + this.getRingAudio().play(); + } + else { + this.getRingAudio().pause(); + } + + this.setState({ + incomingCall: call + }); + }, + + onAnswerClick: function() { + dis.dispatch({ + action: 'answer', + room_id: this.state.incomingCall.roomId + }); + }, + onRejectClick: function() { + dis.dispatch({ + action: 'hangup', + room_id: this.state.incomingCall.roomId + }); + } +}; + diff --git a/src/controllers/molecules/voip/VideoView.js b/src/controllers/molecules/voip/VideoView.js new file mode 100644 index 0000000000..3d34134daa --- /dev/null +++ b/src/controllers/molecules/voip/VideoView.js @@ -0,0 +1,19 @@ +/* +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. +*/ + +module.exports = { +}; + diff --git a/src/controllers/organisms/RoomView.js b/src/controllers/organisms/RoomView.js index a40c1e633a..6b3955d41e 100644 --- a/src/controllers/organisms/RoomView.js +++ b/src/controllers/organisms/RoomView.js @@ -14,31 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - var MatrixClientPeg = require("../../MatrixClientPeg"); var React = require("react"); var q = require("q"); var ContentMessages = require("../../ContentMessages"); +var WhoIsTyping = require("../../WhoIsTyping"); +var Modal = require("../../Modal"); +var sdk = require('../../index'); var dis = require("../../dispatcher"); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 100; -var sdk = require('../../index'); - module.exports = { getInitialState: function() { return { room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null, - messageCap: INITIAL_SIZE + messageCap: INITIAL_SIZE, + editingRoomSettings: false, + uploadingRoomSettings: false, + numUnreadMessages: 0, + draggingFile: false, } }, componentWillMount: function() { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().on("Room.name", this.onRoomName); + MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); this.atBottom = true; }, @@ -46,19 +51,40 @@ module.exports = { if (this.refs.messageWrapper) { var messageWrapper = this.refs.messageWrapper.getDOMNode(); messageWrapper.removeEventListener('drop', this.onDrop); + messageWrapper.removeEventListener('dragover', this.onDragOver); + messageWrapper.removeEventListener('dragleave', this.onDragLeaveOrEnd); + messageWrapper.removeEventListener('dragend', this.onDragLeaveOrEnd); } dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); + MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); + MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); } }, onAction: function(payload) { switch (payload.action) { + case 'message_send_failed': case 'message_sent': this.setState({ room: MatrixClientPeg.get().getRoom(this.props.roomId) }); + this.forceUpdate(); + break; + case 'notifier_enabled': + this.forceUpdate(); + break; + case 'call_state': + if (this.props.roomId !== payload.room_id) { + break; + } + // scroll to bottom + var messageWrapper = this.refs.messageWrapper; + if (messageWrapper) { + messageWrapper = messageWrapper.getDOMNode(); + messageWrapper.scrollTop = messageWrapper.scrollHeight; + } break; } }, @@ -82,13 +108,31 @@ module.exports = { // we'll only be showing a spinner. if (this.state.joining) return; if (room.roomId != this.props.roomId) return; - + if (this.refs.messageWrapper) { var messageWrapper = this.refs.messageWrapper.getDOMNode(); - this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight; + this.atBottom = ( + messageWrapper.scrollHeight - messageWrapper.scrollTop <= + (messageWrapper.clientHeight + 150) + ); } + + var currentUnread = this.state.numUnreadMessages; + if (!toStartOfTimeline && + (ev.getSender() !== MatrixClientPeg.get().credentials.userId)) { + // update unread count when scrolled up + if (this.atBottom) { + currentUnread = 0; + } + else { + currentUnread += 1; + } + } + + this.setState({ - room: MatrixClientPeg.get().getRoom(this.props.roomId) + room: MatrixClientPeg.get().getRoom(this.props.roomId), + numUnreadMessages: currentUnread }); if (toStartOfTimeline && !this.state.paginating) { @@ -96,12 +140,26 @@ module.exports = { } }, + onRoomName: function(room) { + if (room.roomId == this.props.roomId) { + this.setState({ + room: room + }); + } + }, + + onRoomMemberTyping: function(ev, member) { + this.forceUpdate(); + }, + componentDidMount: function() { if (this.refs.messageWrapper) { var messageWrapper = this.refs.messageWrapper.getDOMNode(); messageWrapper.addEventListener('drop', this.onDrop); messageWrapper.addEventListener('dragover', this.onDragOver); + messageWrapper.addEventListener('dragleave', this.onDragLeaveOrEnd); + messageWrapper.addEventListener('dragend', this.onDragLeaveOrEnd); messageWrapper.scrollTop = messageWrapper.scrollHeight; @@ -123,10 +181,14 @@ module.exports = { } } else if (this.atBottom) { messageWrapper.scrollTop = messageWrapper.scrollHeight; + if (this.state.numUnreadMessages !== 0) { + this.setState({numUnreadMessages: 0}); + } } }, fillSpace: function() { + if (!this.refs.messageWrapper) return; var messageWrapper = this.refs.messageWrapper.getDOMNode(); if (messageWrapper.scrollTop < messageWrapper.clientHeight && this.state.room.oldState.paginationToken) { this.setState({paginating: true}); @@ -141,12 +203,12 @@ module.exports = { this.waiting_for_paginate = true; var cap = this.state.messageCap + PAGINATE_SIZE; this.setState({messageCap: cap, paginating: true}); - var that = this; + var self = this; MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(function() { - that.waiting_for_paginate = false; - if (that.isMounted()) { - that.setState({ - room: MatrixClientPeg.get().getRoom(that.props.roomId) + self.waiting_for_paginate = false; + if (self.isMounted()) { + self.setState({ + room: MatrixClientPeg.get().getRoom(self.props.roomId) }); } // wait and set paginating to false when the component updates @@ -159,14 +221,14 @@ module.exports = { }, onJoinButtonClicked: function(ev) { - var that = this; + var self = this; MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() { - that.setState({ + self.setState({ joining: false, - room: MatrixClientPeg.get().getRoom(that.props.roomId) + room: MatrixClientPeg.get().getRoom(self.props.roomId) }); }, function(error) { - that.setState({ + self.setState({ joining: false, joinError: error }); @@ -179,7 +241,11 @@ module.exports = { onMessageListScroll: function(ev) { if (this.refs.messageWrapper) { var messageWrapper = this.refs.messageWrapper.getDOMNode(); + var wasAtBottom = this.atBottom; this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight; + if (this.atBottom && !wasAtBottom) { + this.forceUpdate(); // remove unread msg count + } } if (!this.state.paginating) this.fillSpace(); }, @@ -193,6 +259,7 @@ module.exports = { var items = ev.dataTransfer.items; if (items.length == 1) { if (items[0].kind == 'file') { + this.setState({ draggingFile : true }); ev.dataTransfer.dropEffect = 'copy'; } } @@ -201,23 +268,60 @@ module.exports = { onDrop: function(ev) { ev.stopPropagation(); ev.preventDefault(); + this.setState({ draggingFile : false }); var files = ev.dataTransfer.files; - if (files.length == 1) { - ContentMessages.sendContentToRoom( - files[0], this.props.roomId, MatrixClientPeg.get() - ).progress(function(ev) { - //console.log("Upload: "+ev.loaded+" / "+ev.total); - }).done(undefined, function() { - // display error message - }); + this.uploadFile(files[0]); } }, + onDragLeaveOrEnd: function(ev) { + ev.stopPropagation(); + ev.preventDefault(); + this.setState({ draggingFile : false }); + }, + + uploadFile: function(file) { + this.setState({ + upload: { + fileName: file.name, + uploadedBytes: 0, + totalBytes: file.size + } + }); + var self = this; + ContentMessages.sendContentToRoom( + file, this.props.roomId, MatrixClientPeg.get() + ).progress(function(ev) { + //console.log("Upload: "+ev.loaded+" / "+ev.total); + self.setState({ + upload: { + fileName: file.name, + uploadedBytes: ev.loaded, + totalBytes: ev.total + } + }); + }).finally(function() { + self.setState({ + upload: undefined + }); + }).done(undefined, function() { + // display error message + }); + }, + + getWhoIsTypingString: function() { + return WhoIsTyping.whoIsTypingString(this.state.room); + }, + getEventTiles: function() { var tileTypes = { 'm.room.message': sdk.getComponent('molecules.MessageTile'), - 'm.room.member': sdk.getComponent('molecules.MRoomMemberTile') + 'm.room.member' : sdk.getComponent('molecules.EventAsTextTile'), + 'm.call.invite' : sdk.getComponent('molecules.EventAsTextTile'), + 'm.call.answer' : sdk.getComponent('molecules.EventAsTextTile'), + 'm.call.hangup' : sdk.getComponent('molecules.EventAsTextTile'), + 'm.room.topic' : sdk.getComponent('molecules.EventAsTextTile'), }; var ret = []; @@ -226,13 +330,119 @@ module.exports = { for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) { var mxEv = this.state.room.timeline[i]; var TileType = tileTypes[mxEv.getType()]; + var continuation = false; + var last = false; + if (i == this.state.room.timeline.length - 1) { + last = true; + } + if (i > 0 && count < this.state.messageCap - 1) { + if (this.state.room.timeline[i].sender && + this.state.room.timeline[i - 1].sender && + (this.state.room.timeline[i].sender.userId === + this.state.room.timeline[i - 1].sender.userId) && + (this.state.room.timeline[i].getType() == + this.state.room.timeline[i - 1].getType()) + ) + { + continuation = true; + } + + var ts0 = this.state.room.timeline[i - 1].getTs(); + var ts1 = this.state.room.timeline[i].getTs(); + } if (!TileType) continue; ret.unshift( - +
  • ); ++count; } return ret; + }, + + uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels) { + var old_name = this.state.room.name; + + var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', ''); + if (old_topic) { + old_topic = old_topic.getContent().topic; + } else { + old_topic = ""; + } + + var old_join_rule = this.state.room.currentState.getStateEvents('m.room.join_rules', ''); + if (old_join_rule) { + old_join_rule = old_join_rule.getContent().join_rule; + } else { + old_join_rule = "invite"; + } + + var old_history_visibility = this.state.room.currentState.getStateEvents('m.room.history_visibility', ''); + if (old_history_visibility) { + old_history_visibility = old_history_visibility.getContent().history_visibility; + } else { + old_history_visibility = "shared"; + } + + var deferreds = []; + + if (old_name != new_name && new_name != undefined && new_name) { + deferreds.push( + MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name) + ); + } + + if (old_topic != new_topic && new_topic != undefined) { + deferreds.push( + MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic) + ); + } + + if (old_join_rule != new_join_rule && new_join_rule != undefined) { + deferreds.push( + MatrixClientPeg.get().sendStateEvent( + this.state.room.roomId, "m.room.join_rules", { + join_rule: new_join_rule, + }, "" + ) + ); + } + + if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) { + deferreds.push( + MatrixClientPeg.get().sendStateEvent( + this.state.room.roomId, "m.room.history_visibility", { + history_visibility: new_history_visibility, + }, "" + ) + ); + } + + if (new_power_levels) { + deferreds.push( + MatrixClientPeg.get().sendStateEvent( + this.state.room.roomId, "m.room.power_levels", new_power_levels, "" + ) + ); + } + + if (deferreds.length) { + var self = this; + q.all(deferreds).fail(function(err) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Failed to set state", + description: err.toString() + }); + }).finally(function() { + self.setState({ + uploadingRoomSettings: false, + }); + }); + } else { + this.setState({ + editingRoomSettings: false, + uploadingRoomSettings: false, + }); + } } }; -