diff --git a/package.json b/package.json index 0b5a18168f..fda905f94e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "q": "^1.4.1", "react": "^0.14.2", "react-dom": "^0.14.2", + "react-gemini-scrollbar": "^2.0.1", "sanitize-html": "^1.11.1", "velocity-animate": "^1.2.3" }, diff --git a/src/CallHandler.js b/src/CallHandler.js index b3af0e8337..187449924f 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -56,12 +56,12 @@ var Modal = require('./Modal'); var sdk = require('./index'); var Matrix = require("matrix-js-sdk"); var dis = require("./dispatcher"); -var Modulator = require("./Modulator"); global.mxCalls = { //room_id: MatrixCall }; var calls = global.mxCalls; +var ConferenceHandler = null; function play(audioId) { // TODO: Attach an invisible element for this instead @@ -115,7 +115,7 @@ function _setCallListeners(call) { _setCallState(call, call.roomId, "busy"); pause("ringbackAudio"); play("busyAudio"); - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Call Timeout", description: "The remote side failed to pick up." @@ -173,7 +173,7 @@ function _onAction(payload) { console.error("Unknown conf call type: %s", payload.type); } } - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (payload.action) { case 'place_call': @@ -202,7 +202,7 @@ function _onAction(payload) { var members = room.getJoinedMembers(); if (members.length <= 1) { - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { description: "You cannot place a call with yourself." }); @@ -227,7 +227,7 @@ function _onAction(payload) { break; case 'place_conference_call': console.log("Place conference call in %s", payload.room_id); - if (!Modulator.hasConferenceHandler()) { + if (!ConferenceHandler) { Modal.createDialog(ErrorDialog, { description: "Conference calls are not supported in this client" }); @@ -239,7 +239,6 @@ function _onAction(payload) { }); } else { - var ConferenceHandler = Modulator.getConferenceHandler(); ConferenceHandler.createNewMatrixCall( MatrixClientPeg.get(), payload.room_id ).done(function(call) { @@ -295,8 +294,7 @@ var callHandler = { var call = module.exports.getCall(roomId); if (call) return call; - if (Modulator.hasConferenceHandler()) { - var ConferenceHandler = Modulator.getConferenceHandler(); + if (ConferenceHandler) { call = ConferenceHandler.getConferenceCallForRoom(roomId); } if (call) return call; @@ -317,6 +315,10 @@ var callHandler = { } } return null; + }, + + setConferenceHandler: function(confHandler) { + ConferenceHandler = confHandler; } }; // Only things in here which actually need to be global are the diff --git a/src/DateUtils.js b/src/DateUtils.js new file mode 100644 index 0000000000..fe363586ab --- /dev/null +++ b/src/DateUtils.js @@ -0,0 +1,45 @@ +/* +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 days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + +module.exports = { + formatDate: function(date) { + // date.toLocaleTimeString is completely system dependent. + // just go 24h for now + function pad(n) { + return (n < 10 ? '0' : '') + n; + } + + var now = new Date(); + if (date.toDateString() === now.toDateString()) { + return pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { + return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + else if (now.getFullYear() === date.getFullYear()) { + return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + else { + return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + } + } +} + diff --git a/src/Modulator.js b/src/Modulator.js deleted file mode 100644 index 72fcc14d89..0000000000 --- a/src/Modulator.js +++ /dev/null @@ -1,111 +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. -*/ - -/** - * The modulator stores 'modules': classes that provide - * functionality and are not React UI components. - * Modules go into named slots, eg. a conference calling - * module goes into the 'conference' slot. If two modules - * that use the same slot are loaded, this is considered - * to be an error. - * - * There are some module slots that the react SDK knows - * about natively: these have explicit getters. - * - * A module must define: - * - 'slot' (string): The name of the slot it goes into - * and may define: - * - 'start' (function): Called on module load - * - 'stop' (function): Called on module unload - */ -class Modulator { - constructor() { - this.modules = {}; - } - - getModule(name) { - var m = this.getModuleOrNull(name); - if (m === null) { - throw new Error("No such module: "+name); - } - return m; - } - - getModuleOrNull(name) { - if (this.modules == {}) { - throw new Error( - "Attempted to get a module before a skin has been loaded."+ - "This is probably because a component has called "+ - "getModule at the root level." - ); - } - var module = this.modules[name]; - if (module) { - return module; - } - return null; - } - - hasModule(name) { - var m = this.getModuleOrNull(name); - return m !== null; - } - - loadModule(moduleObject) { - if (!moduleObject.slot) { - throw new Error( - "Attempted to load something that is not a module "+ - "(does not have a slot name)" - ); - } - if (this.modules[moduleObject.slot] !== undefined) { - throw new Error( - "Cannot load module: slot '"+moduleObject.slot+"' is occupied!" - ); - } - this.modules[moduleObject.slot] = moduleObject; - } - - reset() { - var keys = Object.keys(this.modules); - for (var i = 0; i < keys.length; ++i) { - var k = keys[i]; - var m = this.modules[k]; - - if (m.stop) m.stop(); - } - this.modules = {}; - } - - // *********** - // known slots - // *********** - - getConferenceHandler() { - return this.getModule('conference'); - } - - hasConferenceHandler() { - return this.hasModule('conference'); - } -} - -// Define one Modulator globally (see Skinner.js) -if (global.mxModulator === undefined) { - global.mxModulator = new Modulator(); -} -module.exports = global.mxModulator; - diff --git a/src/controllers/organisms/Notifier.js b/src/Notifier.js similarity index 59% rename from src/controllers/organisms/Notifier.js rename to src/Notifier.js index 8fb62abe40..66e96fb15c 100644 --- a/src/controllers/organisms/Notifier.js +++ b/src/Notifier.js @@ -16,8 +16,10 @@ limitations under the License. 'use strict'; -var MatrixClientPeg = require("../../MatrixClientPeg"); -var dis = require("../../dispatcher"); +var MatrixClientPeg = require("./MatrixClientPeg"); +var TextForEvent = require('./TextForEvent'); +var Avatar = require('./Avatar'); +var dis = require("./dispatcher"); /* * Dispatches: @@ -28,10 +30,76 @@ var dis = require("../../dispatcher"); */ module.exports = { + + notificationMessageForEvent: function(ev) { + return TextForEvent.textForEvent(ev); + }, + + displayNotification: function(ev, room) { + if (!global.Notification || global.Notification.permission != 'granted') { + return; + } + if (global.document.hasFocus()) { + return; + } + + var msg = this.notificationMessageForEvent(ev); + if (!msg) return; + + var title; + if (!ev.sender || room.name == ev.sender.name) { + title = room.name; + // notificationMessageForEvent includes sender, + // but we already have the sender here + if (ev.getContent().body) msg = ev.getContent().body; + } else if (ev.getType() == 'm.room.member') { + // context is all in the message here, we don't need + // to display sender info + title = room.name; + } else if (ev.sender) { + title = ev.sender.name + " (" + room.name + ")"; + // notificationMessageForEvent includes sender, + // but we've just out sender in the title + if (ev.getContent().body) msg = ev.getContent().body; + } + + var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( + ev.sender, 40, 40, 'crop' + ) : null; + + var notification = new global.Notification( + title, + { + "body": msg, + "icon": avatarUrl, + "tag": "matrixreactsdk" + } + ); + + notification.onclick = function() { + dis.dispatch({ + action: 'view_room', + room_id: room.roomId + }); + global.focus(); + }; + + /*var audioClip; + + if (audioNotification) { + audioClip = playAudio(audioNotification); + }*/ + + global.setTimeout(function() { + notification.close(); + }, 5 * 1000); + + }, + start: function() { this.boundOnRoomTimeline = this.onRoomTimeline.bind(this); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); - this.state = { 'toolbarHidden' : false }; + this.toolbarHidden = false; }, stop: function() { @@ -96,7 +164,7 @@ module.exports = { }, setToolbarHidden: function(hidden) { - this.state.toolbarHidden = hidden; + this.toolbarHidden = hidden; dis.dispatch({ action: "notifier_enabled", value: this.isEnabled() @@ -104,7 +172,7 @@ module.exports = { }, isToolbarHidden: function() { - return this.state.toolbarHidden; + return this.toolbarHidden; }, onRoomTimeline: function(ev, room, toStartOfTimeline) { diff --git a/src/Resend.js b/src/Resend.js new file mode 100644 index 0000000000..b1132750b8 --- /dev/null +++ b/src/Resend.js @@ -0,0 +1,24 @@ +var MatrixClientPeg = require('./MatrixClientPeg'); +var dis = require('./dispatcher'); + +module.exports = { + resend: function(event) { + MatrixClientPeg.get().resendEvent( + event, MatrixClientPeg.get().getRoom(event.getRoomId()) + ).done(function() { + dis.dispatch({ + action: 'message_sent', + event: event + }); + }, function() { + dis.dispatch({ + action: 'message_send_failed', + event: event + }); + }); + dis.dispatch({ + action: 'message_resend_started', + event: event + }); + }, +}; \ No newline at end of file diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js new file mode 100644 index 0000000000..d6002fb84c --- /dev/null +++ b/src/components/structures/CreateRoom.js @@ -0,0 +1,290 @@ +/* +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 PresetValues = { + PrivateChat: "private_chat", + PublicChat: "public_chat", + Custom: "custom", +}; +var q = require('q'); +var encryption = require("../../encryption"); +var sdk = require('../../index'); + +module.exports = React.createClass({ + displayName: 'CreateRoom', + + propTypes: { + onRoomCreated: React.PropTypes.func, + }, + + phases: { + CONFIG: "CONFIG", // We're waiting for user to configure and hit create. + CREATING: "CREATING", // We're sending the request. + CREATED: "CREATED", // We successfully created the room. + ERROR: "ERROR", // There was an error while trying to create room. + }, + + getDefaultProps: function() { + return { + onRoomCreated: function() {}, + }; + }, + + getInitialState: function() { + return { + phase: this.phases.CONFIG, + error_string: "", + is_private: true, + share_history: false, + default_preset: PresetValues.PrivateChat, + topic: '', + room_name: '', + invited_users: [], + }; + }, + + onCreateRoom: function() { + var options = {}; + + if (this.state.room_name) { + options.name = this.state.room_name; + } + + if (this.state.topic) { + options.topic = this.state.topic; + } + + if (this.state.preset) { + if (this.state.preset != PresetValues.Custom) { + options.preset = this.state.preset; + } else { + options.initial_state = [ + { + type: "m.room.join_rules", + content: { + "join_rule": this.state.is_private ? "invite" : "public" + } + }, + { + type: "m.room.history_visibility", + content: { + "history_visibility": this.state.share_history ? "shared" : "invited" + } + }, + ]; + } + } + + options.invite = this.state.invited_users; + + var alias = this.getAliasLocalpart(); + if (alias) { + options.room_alias_name = alias; + } + + var cli = MatrixClientPeg.get(); + if (!cli) { + // TODO: Error. + console.error("Cannot create room: No matrix client."); + return; + } + + var deferred = cli.createRoom(options); + + var response; + + if (this.state.encrypt) { + deferred = deferred.then(function(res) { + response = res; + return encryption.enableEncryption( + cli, response.room_id, options.invite + ); + }).then(function() { + return q(response) } + ); + } + + this.setState({ + phase: this.phases.CREATING, + }); + + var self = this; + + deferred.then(function (resp) { + self.setState({ + phase: self.phases.CREATED, + }); + self.props.onRoomCreated(resp.room_id); + }, function(err) { + self.setState({ + phase: self.phases.ERROR, + error_string: err.toString(), + }); + }); + }, + + getPreset: function() { + return this.refs.presets.getPreset(); + }, + + getName: function() { + return this.refs.name_textbox.getName(); + }, + + getTopic: function() { + return this.refs.topic.getTopic(); + }, + + getAliasLocalpart: function() { + return this.refs.alias.getAliasLocalpart(); + }, + + getInvitedUsers: function() { + return this.refs.user_selector.getUserIds(); + }, + + onPresetChanged: function(preset) { + switch (preset) { + case PresetValues.PrivateChat: + this.setState({ + preset: preset, + is_private: true, + share_history: false, + }); + break; + case PresetValues.PublicChat: + this.setState({ + preset: preset, + is_private: false, + share_history: true, + }); + break; + case PresetValues.Custom: + this.setState({ + preset: preset, + }); + break; + } + }, + + onPrivateChanged: function(ev) { + this.setState({ + preset: PresetValues.Custom, + is_private: ev.target.checked, + }); + }, + + onShareHistoryChanged: function(ev) { + this.setState({ + preset: PresetValues.Custom, + share_history: ev.target.checked, + }); + }, + + onTopicChange: function(ev) { + this.setState({ + topic: ev.target.value, + }); + }, + + onNameChange: function(ev) { + this.setState({ + room_name: ev.target.value, + }); + }, + + onInviteChanged: function(invited_users) { + this.setState({ + invited_users: invited_users, + }); + }, + + onAliasChanged: function(alias) { + this.setState({ + alias: alias + }) + }, + + onEncryptChanged: function(ev) { + this.setState({ + encrypt: ev.target.checked, + }); + }, + + render: function() { + var curr_phase = this.state.phase; + if (curr_phase == this.phases.CREATING) { + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + } else { + var error_box = ""; + if (curr_phase == this.phases.ERROR) { + error_box = ( +
+ An error occured: {this.state.error_string} +
+ ); + } + + var CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton"); + var RoomAlias = sdk.getComponent("create_room.RoomAlias"); + var Presets = sdk.getComponent("create_room.Presets"); + var UserSelector = sdk.getComponent("elements.UserSelector"); + var RoomHeader = sdk.getComponent("rooms.RoomHeader"); + + return ( +
+ +
+
+