From ef00a1624d34604e6883e8a20b9da26a51efc5fb Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 8 Jan 2016 03:22:38 +0000 Subject: [PATCH 01/82] fix up RoomSettings somewhat and implement room colors --- src/SlashCommands.js | 24 ++- src/Tinter.js | 12 ++ src/components/structures/MatrixChat.js | 10 + src/components/structures/RoomView.js | 45 ++++- src/components/views/rooms/RoomHeader.js | 4 +- src/components/views/rooms/RoomSettings.js | 171 ++++++++++++++---- src/components/views/settings/ChangeAvatar.js | 7 +- 7 files changed, 231 insertions(+), 42 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 363560f0c6..35216aa403 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -43,11 +43,27 @@ var commands = { return reject("Usage: /nick "); }, - // Takes an #rrggbb colourcode and retints the UI (just for debugging) + // Changes the colorscheme of your current room tint: function(room_id, args) { - Tinter.tint(args); - return success(); - }, + + if (args) { + var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); + if (matches) { + Tinter.tint(matches[1], matches[4]); + var colorScheme = {} + colorScheme.primary_color = matches[1]; + if (matches[4]) { + colorScheme.secondary_color = matches[4]; + } + return success( + MatrixClientPeg.get().setRoomAccountData( + room_id, "m.room.color_scheme", colorScheme + ) + ); + } + } + return reject("Usage: /tint []"); + }, encrypt: function(room_id, args) { if (args == "on") { diff --git a/src/Tinter.js b/src/Tinter.js index a9b754ffde..817518d6b2 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -126,6 +126,11 @@ module.exports = { cached = true; } + if (!primaryColor) { + primaryColor = "#76CFA6"; // Vector green + secondaryColor = "#EAF5F0"; // Vector light green + } + if (!secondaryColor) { var x = 0.16; // average weighting factor calculated from vector green & light green var rgb = hexToRgb(primaryColor); @@ -145,6 +150,13 @@ module.exports = { tertiaryColor = rgbToHex(rgb1); } + if (colors[0] === primaryColor && + colors[1] === secondaryColor && + colors[2] === tertiaryColor) + { + return; + } + colors = [primaryColor, secondaryColor, tertiaryColor]; // go through manually fixing up the stylesheets. diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index d732a54922..56885ce499 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -30,6 +30,7 @@ var Registration = require("./login/Registration"); var PostRegistration = require("./login/PostRegistration"); var Modal = require("../../Modal"); +var Tinter = require("../../Tinter"); var sdk = require('../../index'); var MatrixTools = require('../../MatrixTools'); var linkifyMatrix = require("../../linkify-matrix"); @@ -358,7 +359,16 @@ module.exports = React.createClass({ if (room) { var theAlias = MatrixTools.getCanonicalAliasForRoom(room); if (theAlias) presentedId = theAlias; + + var color_scheme_event = room.getAccountData("m.room.color_scheme"); + var color_scheme = {}; + if (color_scheme_event) { + color_scheme = color_scheme_event.getContent(); + // XXX: we should validate the event + } + Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } + this.notifyNewScreen('room/'+presentedId); newState.ready = true; } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9ab99d9cba..08dfe75584 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -37,6 +37,7 @@ var TabComplete = require("../../TabComplete"); var MemberEntry = require("../../TabCompleteEntries").MemberEntry; var Resend = require("../../Resend"); var dis = require("../../dispatcher"); +var Tinter = require("../../Tinter"); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 20; @@ -81,6 +82,7 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); + MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); @@ -115,6 +117,7 @@ module.exports = React.createClass({ if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); + MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); @@ -122,6 +125,8 @@ module.exports = React.createClass({ } window.removeEventListener('resize', this.onResize); + + Tinter.tint(); // reset colourscheme }, onAction: function(payload) { @@ -235,6 +240,29 @@ module.exports = React.createClass({ } }, + updateTint: function() { + var room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (!room) return; + + var color_scheme_event = room.getAccountData("m.room.color_scheme"); + var color_scheme = {}; + if (color_scheme_event) { + color_scheme = color_scheme_event.getContent(); + // XXX: we should validate the event + } + Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + }, + + onRoomAccountData: function(room, event) { + if (room.roomId == this.props.roomId) { + if (event.getType === "m.room.color_scheme") { + var color_scheme = event.getContent(); + // XXX: we should validate the event + Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + } + } + }, + onRoomReceipt: function(receiptEvent, room) { if (room.roomId == this.props.roomId) { this.forceUpdate(); @@ -337,6 +365,8 @@ module.exports = React.createClass({ this.scrollToBottom(); this.sendReadReceipt(); + + this.updateTint(); }, componentDidUpdate: function() { @@ -711,7 +741,7 @@ module.exports = React.createClass({ return ret; }, - uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels) { + uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels, new_color_scheme) { var old_name = this.state.room.name; var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', ''); @@ -777,6 +807,14 @@ module.exports = React.createClass({ ); } + if (new_color_scheme) { + deferreds.push( + MatrixClientPeg.get().setRoomAccountData( + this.state.room.roomId, "m.room.color_scheme", new_color_scheme + ) + ); + } + if (deferreds.length) { var self = this; q.all(deferreds).fail(function(err) { @@ -866,17 +904,20 @@ module.exports = React.createClass({ var new_join_rule = this.refs.room_settings.getJoinRules(); var new_history_visibility = this.refs.room_settings.getHistoryVisibility(); var new_power_levels = this.refs.room_settings.getPowerLevels(); + var new_color_scheme = this.refs.room_settings.getColorScheme(); this.uploadNewState( new_name, new_topic, new_join_rule, new_history_visibility, - new_power_levels + new_power_levels, + new_color_scheme ); }, onCancelClick: function() { + this.updateTint(); this.setState({editingRoomSettings: false}); }, diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8b5435e46a..cdfbc0bfc8 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -116,7 +116,7 @@ module.exports = React.createClass({ } name = -
+
{ this.props.room.name }
{ searchStatus }
@@ -151,7 +151,7 @@ module.exports = React.createClass({ header =
-
+
{ roomAvatar }
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 211ecbd71a..0864dc15c7 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -16,8 +16,23 @@ limitations under the License. var React = require('react'); var MatrixClientPeg = require('../../../MatrixClientPeg'); +var Tinter = require('../../../Tinter'); var sdk = require('../../../index'); +var room_colors = [ + // magic room default values courtesy of Ribot + ["#76cfa6", "#eaf5f0"], + ["#81bddb", "#eaf1f4"], + ["#bd79cb", "#f3eaf5"], + ["#c65d94", "#f5eaef"], + ["#e55e5e", "#f5eaea"], + ["#eca46f", "#f5eeea"], + ["#dad658", "#f5f4ea"], + ["#80c553", "#eef5ea"], + ["#bb814e", "#eee8e3"], + ["#595959", "#ececec"], +]; + module.exports = React.createClass({ displayName: 'RoomSettings', @@ -26,8 +41,37 @@ module.exports = React.createClass({ }, getInitialState: function() { + // work out the initial color index + var room_color_index = undefined; + var color_scheme_event = this.props.room.getAccountData("m.room.color_scheme"); + if (color_scheme_event) { + var color_scheme = color_scheme_event.getContent(); + if (color_scheme.primary_color) color_scheme.primary_color = color_scheme.primary_color.toLowerCase(); + if (color_scheme.secondary_color) color_scheme.secondary_color = color_scheme.secondary_color.toLowerCase(); + // XXX: we should validate these values + for (var i = 0; i < room_colors.length; i++) { + var room_color = room_colors[i]; + if (room_color[0] === color_scheme.primary_color && + room_color[1] === color_scheme.secondary_color) + { + room_color_index = i; + break; + } + } + if (room_color_index === undefined) { + // append the unrecognised colours to our palette + room_color_index = room_colors.length; + room_colors[room_color_index] = [ color_scheme.primary_color, color_scheme.secondary_color ]; + } + } + else { + room_color_index = 0; + } + return { - power_levels_changed: false + power_levels_changed: false, + color_scheme_changed: false, + color_scheme_index: room_color_index, }; }, @@ -70,6 +114,25 @@ module.exports = React.createClass({ }); }, + getColorScheme: function() { + if (!this.state.color_scheme_changed) return undefined; + + return { + primary_color: room_colors[this.state.color_scheme_index][0], + secondary_color: room_colors[this.state.color_scheme_index][1], + }; + }, + + onColorSchemeChanged: function(index) { + // preview what the user just changed the scheme to. + Tinter.tint(room_colors[index][0], room_colors[index][1]); + + this.setState({ + color_scheme_changed: true, + color_scheme_index: index, + }); + }, + render: function() { var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); @@ -139,25 +202,91 @@ module.exports = React.createClass({ } var can_set_room_avatar = current_user_level >= room_avatar_level; + var self = this; + + var room_colors_section = +
+

Room Colour

+
+ {room_colors.map(function(room_color, i) { + var selected; + if (i === self.state.color_scheme_index) { + selected = +
+ ./ +
+ } + var boundClick = self.onColorSchemeChanged.bind(this, i) + return ( +
+ { selected } +
+
+ ); + })} +
+
; + var change_avatar; if (can_set_room_avatar) { - change_avatar =
-

Room Icon

- -
; + change_avatar = +
+

Room Icon

+ +
; } var banned = this.props.room.getMembersWithMembership("ban"); + var events_levels_section; + if (events_levels.length) { + events_levels_section = +
+

Event levels

+
+ {Object.keys(events_levels).map(function(event_type, i) { + return ( +
+ + +
+ ); + })} +
+
; + } + + var banned_users_section; + if (banned.length) { + banned_users_section = +
+

Banned users

+
+ {banned.map(function(member, i) { + return ( +
+ {member.userId} +
+ ); + })} +
+
; + } + return (
- cancel_button =
Cancel
- save_button =
Save Changes
+ + name = +
+ +
+ + topic_el = + + + save_button =
Save
+ cancel_button =
Cancel
} else { // - var searchStatus; // don't display the search count until the search completes and // gives us a valid (possibly zero) searchCount. @@ -123,7 +158,9 @@ module.exports = React.createClass({
- if (topic) topic_el =
{ topic.getContent().topic }
; + + var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); + if (topic) topic_el =
{ topic.getContent().topic }
; } var roomAvatar = null; @@ -149,6 +186,18 @@ module.exports = React.createClass({
; } + var right_row; + if (!this.props.editing) { + right_row = +
+ { forget_button } + { leave_button } +
+ +
+
; + } + header =
@@ -160,15 +209,9 @@ module.exports = React.createClass({ { topic_el }
- {cancel_button} {save_button} -
- { forget_button } - { leave_button } -
- -
-
+ {cancel_button} + {right_row}
} diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 0864dc15c7..c601858757 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -136,9 +136,6 @@ module.exports = React.createClass({ render: function() { var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); - var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); - if (topic) topic = topic.getContent().topic; - var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', ''); if (join_rule) join_rule = join_rule.getContent().join_rule; @@ -216,7 +213,7 @@ module.exports = React.createClass({ ./
} - var boundClick = self.onColorSchemeChanged.bind(this, i) + var boundClick = self.onColorSchemeChanged.bind(self, i) return (
-
+ var placeholderName = this.state.initialName ? "Unnamed Room" : this.props.room.name; + name =
From 6351258b0e6d5677f2a84460af0d6533bf401bb4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Sun, 10 Jan 2016 20:01:30 +0000 Subject: [PATCH 14/82] use room.getImplicitRoomName() from matthew/roomsettings2 branch of matrix-js-sdk for the placeholder roomname --- src/components/views/rooms/RoomHeader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 77ab01afb1..719695bc36 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -48,7 +48,7 @@ module.exports = React.createClass({ this.setState({ name: name ? name.getContent().name : '', - initialName: name ? name.getContent().name : '', + implicitName: this.props.room.getImplicitRoomName(MatrixClientPeg.get().credentials.userId), topic: topic ? topic.getContent().topic : '', }); } @@ -121,7 +121,7 @@ module.exports = React.createClass({ //
// if (topic) topic_el =
- var placeholderName = this.state.initialName ? "Unnamed Room" : this.props.room.name; + var placeholderName = this.state.implicitName || "Unnamed Room"; name =
From ddd8838b24f734514f825307d2898912284a2384 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 11 Jan 2016 12:46:12 +0000 Subject: [PATCH 15/82] linkify topics --- src/components/views/rooms/RoomHeader.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 719695bc36..bf0cc103f8 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -21,6 +21,12 @@ var sdk = require('../../../index'); var dis = require("../../../dispatcher"); var MatrixClientPeg = require('../../../MatrixClientPeg'); +var linkify = require('linkifyjs'); +var linkifyElement = require('linkifyjs/element'); +var linkifyMatrix = require('../../../linkify-matrix'); + +linkifyMatrix(linkify); + module.exports = React.createClass({ displayName: 'RoomHeader', @@ -54,6 +60,12 @@ module.exports = React.createClass({ } }, + componentDidUpdate: function() { + if (this.refs.topic) { + linkifyElement(this.refs.topic, linkifyMatrix.options); + } + }, + onVideoClick: function(e) { dis.dispatch({ action: 'place_call', @@ -121,7 +133,10 @@ module.exports = React.createClass({ //
// if (topic) topic_el =
- var placeholderName = this.state.implicitName || "Unnamed Room"; + var placeholderName = "Unnamed Room"; + if (this.state.implicitName && this.state.implicitName !== '?') { + placeholderName += " (" + this.state.implicitName + ")"; + } name =
@@ -158,13 +173,13 @@ module.exports = React.createClass({
{ this.props.room.name }
{ searchStatus } -
+
var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); - if (topic) topic_el =
{ topic.getContent().topic }
; + if (topic) topic_el =
{ topic.getContent().topic }
; } var roomAvatar = null; @@ -204,7 +219,7 @@ module.exports = React.createClass({ header =
-
+
{ roomAvatar }
From bd226609d047ab72fb95a9a47ff6afd17f27718c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 11 Jan 2016 18:44:36 +0000 Subject: [PATCH 16/82] fix onclick for all of room name --- src/components/views/rooms/RoomHeader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index bf0cc103f8..be227bfe9d 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -170,10 +170,10 @@ module.exports = React.createClass({ } name = -
+
{ this.props.room.name }
{ searchStatus } -
+
From cd525497130aec00f463d7a53be576fedbddeacb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 12 Jan 2016 13:11:53 +0000 Subject: [PATCH 17/82] s/function/func/ in PropTypes declarations --- src/components/views/rooms/MessageComposer.js | 2 +- src/components/views/voip/CallView.js | 2 +- src/components/views/voip/VideoFeed.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index f2894bd6b3..a3ad033acc 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ // a callback which is called when the height of the composer is // changed due to a change in content. - onResize: React.PropTypes.function, + onResize: React.PropTypes.func, }, componentWillMount: function() { diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index 98bad37467..ed44313b9e 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -36,7 +36,7 @@ module.exports = React.createClass({ propTypes: { // a callback which is called when the video within the callview // due to a change in video metadata - onResize: React.PropTypes.function, + onResize: React.PropTypes.func, }, componentDidMount: function() { diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js index 88d1b50cce..c4a65d1145 100644 --- a/src/components/views/voip/VideoFeed.js +++ b/src/components/views/voip/VideoFeed.js @@ -24,7 +24,7 @@ module.exports = React.createClass({ propTypes: { // a callback which is called when the video element is resized // due to a change in video metadata - onResize: React.PropTypes.function, + onResize: React.PropTypes.func, }, componentDidMount() { From eb955eb371275e10dfc5534806d248bfb9ea5444 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 12 Jan 2016 14:11:15 +0000 Subject: [PATCH 18/82] Add a RoomPreviewBar which asks if you'd like to join a peeked room --- src/component-index.js | 9 ++-- src/components/structures/RoomView.js | 28 ++++++++-- src/components/views/rooms/RoomPreviewBar.js | 56 ++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 src/components/views/rooms/RoomPreviewBar.js diff --git a/src/component-index.js b/src/component-index.js index 0c08d70b73..cef1b093a4 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -23,14 +23,14 @@ limitations under the License. module.exports.components = {}; module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); +module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); +module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); +module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.RoomView'] = require('./components/structures/RoomView'); module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); -module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); -module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); -module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton'); @@ -51,10 +51,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin'); module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm'); module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig'); +module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody'); module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); -module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); @@ -65,6 +65,7 @@ module.exports.components['views.rooms.MemberTile'] = require('./components/view module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer'); module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader'); module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList'); +module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar'); module.exports.components['views.rooms.RoomSettings'] = require('./components/views/rooms/RoomSettings'); module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile'); module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile'); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c622a7d769..7a729a4436 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -74,6 +74,7 @@ module.exports = React.createClass({ syncState: MatrixClientPeg.get().getSyncState(), hasUnsentMessages: this._hasUnsentMessages(room), callState: null, + guestsCanJoin: false } }, @@ -106,15 +107,27 @@ module.exports = React.createClass({ // succeeds then great, show the preview (but we still may be able to /join!). if (!this.state.room) { console.log("Attempting to peek into room %s", this.props.roomId); - MatrixClientPeg.get().peekInRoom(this.props.roomId).done(function() { + MatrixClientPeg.get().peekInRoom(this.props.roomId).done(() => { // we don't need to do anything - JS SDK will emit Room events - // which will update the UI. + // which will update the UI. We *do* however need to know if we + // can join the room so we can fiddle with the UI appropriately. + var peekedRoom = MatrixClientPeg.get().getRoom(this.props.roomId); + if (!peekedRoom) { + return; + } + var guestAccessEvent = peekedRoom.currentState.getStateEvents("m.room.guest_access", ""); + if (!guestAccessEvent) { + return; + } + if (guestAccessEvent.getContent().guest_access === "can_join") { + this.setState({ + guestsCanJoin: true + }); + } }, function(err) { console.error("Failed to peek into room: %s", err); }); } - - }, componentWillUnmount: function() { @@ -1132,6 +1145,7 @@ module.exports = React.createClass({ var SearchBar = sdk.getComponent("rooms.SearchBar"); var ScrollPanel = sdk.getComponent("structures.ScrollPanel"); var TintableSvg = sdk.getComponent("elements.TintableSvg"); + var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar"); if (!this.state.room) { if (this.props.roomId) { @@ -1282,6 +1296,12 @@ module.exports = React.createClass({ else if (this.state.searching) { aux = ; } + else if (this.state.guestsCanJoin && MatrixClientPeg.get().isGuest() && + (!myMember || myMember.membership !== "join")) { + aux = ( + + ); + } var conferenceCallNotification = null; if (this.state.displayConfCallNotification) { diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js new file mode 100644 index 0000000000..2f12c4c8e2 --- /dev/null +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -0,0 +1,56 @@ +/* +Copyright 2015, 2016 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'); + +module.exports = React.createClass({ + displayName: 'RoomPreviewBar', + + propTypes: { + onJoinClick: React.PropTypes.func, + canJoin: React.PropTypes.bool + }, + + getDefaultProps: function() { + return { + onJoinClick: function() {}, + canJoin: false + }; + }, + + render: function() { + var joinBlock; + + if (this.props.canJoin) { + joinBlock = ( +
+ Would you like to join this room? +
+ ); + } + + return ( +
+
+ This is a preview of this room. Room interactions have been disabled. +
+ {joinBlock} +
+ ); + } +}); From 37f1b4ba8a884690e93a868655f4eac02be0cd0c Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Jan 2016 14:13:42 +0000 Subject: [PATCH 19/82] Tweaked style means we can have 100% width (well 99% otherwise we gain a horizontal scrollbar) --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 2e6ebd2933..5df5a1044e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -694,7 +694,7 @@ module.exports = React.createClass({ var self = this; if (prevEvent && prevEvent.getId() == this.state.readReceiptEventId) { var hr; - hr = (
); readMarkerIndex = ret.length; From 8b730c0a5d7c1aa2db7ddf0227e0d7aa3fedddfb Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Jan 2016 16:38:09 +0000 Subject: [PATCH 20/82] PR feedback --- src/components/structures/RoomView.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5df5a1044e..e1f8067146 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -75,7 +75,7 @@ module.exports = React.createClass({ syncState: MatrixClientPeg.get().getSyncState(), hasUnsentMessages: this._hasUnsentMessages(room), callState: null, - readReceiptEventId: room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId), + readMarkerEventId: room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId), readMarkerGhostEventId: undefined, } }, @@ -246,13 +246,13 @@ module.exports = React.createClass({ onRoomReceipt: function(receiptEvent, room) { if (room.roomId == this.props.roomId) { - var readReceiptEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); var readMarkerGhostEventId = this.state.readMarkerGhostEventId; - if (this.state.readReceiptEventId !== undefined && this.state.readReceiptEventId != readReceiptEventId) { - readMarkerGhostEventId = this.state.readReceiptEventId; + if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) { + readMarkerGhostEventId = this.state.readMarkerEventId; } this.setState({ - readReceiptEventId: readReceiptEventId, + readMarkerEventId: readMarkerEventId, readMarkerGhostEventId: readMarkerGhostEventId, }); } @@ -683,7 +683,7 @@ module.exports = React.createClass({ } } - // now we've decided whether or not to show this messages, + // now we've decided whether or not to show this message, // add the read up to marker if appropriate // doing this here means we implicitly do not show the marker // if it's at the bottom @@ -692,7 +692,7 @@ module.exports = React.createClass({ // this is where we decide what messages we show so it's the only // place we know whether we're at the bottom or not. var self = this; - if (prevEvent && prevEvent.getId() == this.state.readReceiptEventId) { + if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId) { var hr; hr = (
); From 4a8b5dfe3a4b9d1a663b6fb7a0a68c1fea0dc56d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Jan 2016 17:18:16 +0000 Subject: [PATCH 21/82] Don't display read markers (or ghosts) above our own messages. --- src/components/structures/RoomView.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index e1f8067146..04facc59fc 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -251,6 +251,24 @@ module.exports = React.createClass({ if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) { readMarkerGhostEventId = this.state.readMarkerEventId; } + + + // if the event after the one referenced in the read receipt if sent by us, do nothing since + // this is a temporary period before the synthesized receipt for our own message arrives + var readMarkerGhostEventIndex; + for (var i = 0; i < room.timeline.length; ++i) { + if (room.timeline[i].getId() == readMarkerGhostEventId) { + readMarkerGhostEventIndex = i; + break; + } + } + if (readMarkerGhostEventIndex + 1 < room.timeline.length) { + var nextEvent = room.timeline[readMarkerGhostEventIndex + 1]; + if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) { + readMarkerGhostEventId = undefined; + } + } + this.setState({ readMarkerEventId: readMarkerEventId, readMarkerGhostEventId: readMarkerGhostEventId, @@ -692,7 +710,8 @@ module.exports = React.createClass({ // this is where we decide what messages we show so it's the only // place we know whether we're at the bottom or not. var self = this; - if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId) { + var mxEvSender = mxEv.sender ? mxEv.sender.userId : null; + if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) { var hr; hr = (
Date: Tue, 12 Jan 2016 17:20:16 +0000 Subject: [PATCH 22/82] Implement password reset This adds a link to the login screen with "Forgot your password?". Clicking it takes you to a form with fields for an email address and a new password. This makes the same API calls as the Angular SDK. Manually tested resetting + not clicking link + invalid email and it all seems to work. --- src/PasswordReset.js | 104 +++++++++ src/component-index.js | 9 +- src/components/structures/MatrixChat.js | 27 ++- .../structures/login/ForgotPassword.js | 199 ++++++++++++++++++ src/components/structures/login/Login.js | 8 +- src/components/views/login/PasswordLogin.js | 14 +- 6 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 src/PasswordReset.js create mode 100644 src/components/structures/login/ForgotPassword.js diff --git a/src/PasswordReset.js b/src/PasswordReset.js new file mode 100644 index 0000000000..1029b07b70 --- /dev/null +++ b/src/PasswordReset.js @@ -0,0 +1,104 @@ +/* +Copyright 2015, 2016 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 Matrix = require("matrix-js-sdk"); + +/** + * Allows a user to reset their password on a homeserver. + * + * This involves getting an email token from the identity server to "prove" that + * the client owns the given email address, which is then passed to the password + * API on the homeserver in question with the new password. + */ +class PasswordReset { + + /** + * Configure the endpoints for password resetting. + * @param {string} homeserverUrl The URL to the HS which has the account to reset. + * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. + */ + constructor(homeserverUrl, identityUrl) { + this.client = Matrix.createClient({ + baseUrl: homeserverUrl, + idBaseUrl: identityUrl + }); + this.clientSecret = generateClientSecret(); + this.identityServerDomain = identityUrl.split("://")[1]; + } + + /** + * Attempt to reset the user's password. This will trigger a side-effect of + * sending an email to the provided email address. + * @param {string} emailAddress The email address + * @param {string} newPassword The new password for the account. + * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked(). + */ + resetPassword(emailAddress, newPassword) { + this.password = newPassword; + return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + this.sessionId = res.sid; + return res; + }, function(err) { + if (err.httpStatus) { + err.message = err.message + ` (Status ${err.httpStatus})`; + } + throw err; + }); + } + + /** + * Checks if the email link has been clicked by attempting to change the password + * for the mxid linked to the email. + * @return {Promise} Resolves if the password was reset. Rejects with an object + * with a "message" property which contains a human-readable message detailing why + * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". + */ + checkEmailLinkClicked() { + return this.client.setPassword({ + type: "m.login.email.identity", + threepid_creds: { + sid: this.sessionId, + client_secret: this.clientSecret, + id_server: this.identityServerDomain + } + }, this.password).catch(function(err) { + if (err.httpStatus === 401) { + err.message = "Failed to verify email address: make sure you clicked the link in the email"; + } + else if (err.httpStatus === 404) { + err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver."; + } + else if (err.httpStatus) { + err.message += ` (Status ${err.httpStatus})`; + } + throw err; + }); + } +} + +// from Angular SDK +function generateClientSecret() { + var ret = ""; + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < 32; i++) { + ret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return ret; +} + +module.exports = PasswordReset; diff --git a/src/component-index.js b/src/component-index.js index 0c08d70b73..ac9a83346a 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -23,14 +23,15 @@ limitations under the License. module.exports.components = {}; module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); +module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword'); +module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); +module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); +module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); module.exports.components['structures.RoomView'] = require('./components/structures/RoomView'); module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); -module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); -module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); -module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton'); @@ -51,10 +52,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin'); module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm'); module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig'); +module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody'); module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); -module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 320dad09b3..ede2ab8f46 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -233,6 +233,13 @@ module.exports = React.createClass({ }); this.notifyNewScreen('register'); break; + case 'start_password_recovery': + if (this.state.logged_in) return; + this.replaceState({ + screen: 'forgot_password' + }); + this.notifyNewScreen('forgot_password'); + break; case 'token_login': if (this.state.logged_in) return; @@ -559,6 +566,11 @@ module.exports = React.createClass({ action: 'token_login', params: params }); + } else if (screen == 'forgot_password') { + dis.dispatch({ + action: 'start_password_recovery', + params: params + }); } else if (screen == 'new') { dis.dispatch({ action: 'view_create_room', @@ -668,6 +680,10 @@ module.exports = React.createClass({ this.showScreen("login"); }, + onForgotPasswordClick: function() { + this.showScreen("forgot_password"); + }, + onRegistered: function(credentials) { this.onLoggedIn(credentials); // do post-registration stuff @@ -706,6 +722,7 @@ module.exports = React.createClass({ var CreateRoom = sdk.getComponent('structures.CreateRoom'); var RoomDirectory = sdk.getComponent('structures.RoomDirectory'); var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); + var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); // needs to be before normal PageTypes as you are logged in technically if (this.state.screen == 'post_registration') { @@ -801,13 +818,21 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} /> ); + } else if (this.state.screen == 'forgot_password') { + return ( + + ); } else { return ( + identityServerUrl={this.props.config.default_is_url} + onForgotPasswordClick={this.onForgotPasswordClick} /> ); } } diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js new file mode 100644 index 0000000000..dcf6a7c28e --- /dev/null +++ b/src/components/structures/login/ForgotPassword.js @@ -0,0 +1,199 @@ +/* +Copyright 2015, 2016 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 sdk = require('../../../index'); +var Modal = require("../../../Modal"); +var MatrixClientPeg = require('../../../MatrixClientPeg'); + +var PasswordReset = require("../../../PasswordReset"); + +module.exports = React.createClass({ + displayName: 'ForgotPassword', + + propTypes: { + homeserverUrl: React.PropTypes.string, + identityServerUrl: React.PropTypes.string, + onComplete: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + enteredHomeserverUrl: this.props.homeserverUrl, + enteredIdentityServerUrl: this.props.identityServerUrl, + progress: null + }; + }, + + submitPasswordReset: function(hsUrl, identityUrl, email, password) { + this.setState({ + progress: "sending_email" + }); + this.reset = new PasswordReset(hsUrl, identityUrl); + this.reset.resetPassword(email, password).done(() => { + this.setState({ + progress: "sent_email" + }); + }, (err) => { + this.showErrorDialog("Failed to send email: " + err.message); + this.setState({ + progress: null + }); + }) + }, + + onVerify: function(ev) { + ev.preventDefault(); + if (!this.reset) { + console.error("onVerify called before submitPasswordReset!"); + return; + } + this.reset.checkEmailLinkClicked().done((res) => { + this.setState({ progress: "complete" }); + }, (err) => { + this.showErrorDialog(err.message); + }) + }, + + onSubmitForm: function(ev) { + ev.preventDefault(); + + if (!this.state.email) { + this.showErrorDialog("The email address linked to your account must be entered."); + } + else if (!this.state.password || !this.state.password2) { + this.showErrorDialog("A new password must be entered."); + } + else if (this.state.password !== this.state.password2) { + this.showErrorDialog("New passwords must match each other."); + } + else { + this.submitPasswordReset( + this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl, + this.state.email, this.state.password + ); + } + }, + + onInputChanged: function(stateKey, ev) { + this.setState({ + [stateKey]: ev.target.value + }); + }, + + onHsUrlChanged: function(newHsUrl) { + this.setState({ + enteredHomeserverUrl: newHsUrl + }); + }, + + onIsUrlChanged: function(newIsUrl) { + this.setState({ + enteredIdentityServerUrl: newIsUrl + }); + }, + + showErrorDialog: function(body, title) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: title, + description: body + }); + }, + + render: function() { + var LoginHeader = sdk.getComponent("login.LoginHeader"); + var LoginFooter = sdk.getComponent("login.LoginFooter"); + var ServerConfig = sdk.getComponent("login.ServerConfig"); + var Spinner = sdk.getComponent("elements.Spinner"); + + var resetPasswordJsx; + + if (this.state.progress === "sending_email") { + resetPasswordJsx = + } + else if (this.state.progress === "sent_email") { + resetPasswordJsx = ( +
+ An email has been sent to {this.state.email}. Once you've followed + the link it contains, click below. +
+ +
+ ); + } + else if (this.state.progress === "complete") { + resetPasswordJsx = ( +
+

Your password has been reset.

+

You have been logged out of all devices and will no longer receive push notifications. + To re-enable notifications, re-log in on each device.

+ +
+ ); + } + else { + resetPasswordJsx = ( +
+ To reset your password, enter the email address linked to your account: +
+
+
+ +
+ +
+ +
+ +
+ + +
+
+ ); + } + + + return ( +
+
+ + {resetPasswordJsx} +
+
+ ); + } +}); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b7d2d762a4..b853b8fd95 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login', homeserverUrl: React.PropTypes.string, identityServerUrl: React.PropTypes.string, // login shouldn't know or care how registration is done. - onRegisterClick: React.PropTypes.func.isRequired + onRegisterClick: React.PropTypes.func.isRequired, + // login shouldn't care how password recovery is done. + onForgotPasswordClick: React.PropTypes.func }, getDefaultProps: function() { @@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login', switch (step) { case 'm.login.password': return ( - + ); case 'm.login.cas': return ( diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 3367ac3257..a8751da1a7 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -22,7 +22,8 @@ var ReactDOM = require('react-dom'); */ module.exports = React.createClass({displayName: 'PasswordLogin', propTypes: { - onSubmit: React.PropTypes.func.isRequired // fn(username, password) + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func // fn() }, getInitialState: function() { @@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }, render: function() { + var forgotPasswordJsx; + + if (this.props.onForgotPasswordClick) { + forgotPasswordJsx = ( + + Forgot your password? + + ); + } + return (
@@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', value={this.state.password} onChange={this.onPasswordChanged} placeholder="Password" />
+ {forgotPasswordJsx}
From 848cb30ea4cea8466ccc9e420db85894e5dae305 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 12 Jan 2016 17:48:34 +0000 Subject: [PATCH 23/82] Remove ill-concieved delay before sending read receipts & instead just wait a bit before removing the ghost read marker. --- src/components/structures/RoomView.js | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 04facc59fc..b06a4e23f4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -103,12 +103,6 @@ module.exports = React.createClass({ }, componentWillUnmount: function() { - // if we're waiting to send a read receipt, don't: - // message wasn't on screen for long enough - if (this.sendRRTimer) { - clearTimeout(this.sendRRTimer); - } - if (this.refs.messagePanel) { // disconnect the D&D event listeners from the message panel. This // is really just for hygiene - the messagePanel is going to be @@ -770,7 +764,7 @@ module.exports = React.createClass({ if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) { var hr; hr = (
); @@ -885,20 +879,15 @@ module.exports = React.createClass({ sendReadReceipt: function() { if (!this.state.room) return; - if (this.sendRRTimer) clearTimeout(this.sendRRTimer); - var self = this; - this.sendRRTimer = setTimeout(function() { - self.sendRRTimer = undefined; - var currentReadUpToEventId = self.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); - var currentReadUpToEventIndex = self._indexForEventId(currentReadUpToEventId); + var currentReadUpToEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); - var lastReadEventIndex = self._getLastDisplayedEventIndexIgnoringOwn(); - if (lastReadEventIndex === null) return; + var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn(); + if (lastReadEventIndex === null) return; - if (lastReadEventIndex > currentReadUpToEventIndex) { - MatrixClientPeg.get().sendReadReceipt(self.state.room.timeline[lastReadEventIndex]); - } - }, SEND_READ_RECEIPT_DELAY); + if (lastReadEventIndex > currentReadUpToEventIndex) { + MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]); + } }, _getLastDisplayedEventIndexIgnoringOwn: function() { From 3934b42ac88e975823b9bed5b3e46239d67ba1b4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Jan 2016 13:01:00 +0000 Subject: [PATCH 24/82] s/m.room.color_scheme/org.matrix.room.color_scheme/g # to make kegan happier --- src/SlashCommands.js | 2 +- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 6 +++--- src/components/views/rooms/RoomSettings.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 35216aa403..1dd7ecb08f 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -57,7 +57,7 @@ var commands = { } return success( MatrixClientPeg.get().setRoomAccountData( - room_id, "m.room.color_scheme", colorScheme + room_id, "org.matrix.room.color_scheme", colorScheme ) ); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 56885ce499..ef77787035 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -360,7 +360,7 @@ module.exports = React.createClass({ var theAlias = MatrixTools.getCanonicalAliasForRoom(room); if (theAlias) presentedId = theAlias; - var color_scheme_event = room.getAccountData("m.room.color_scheme"); + var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); var color_scheme = {}; if (color_scheme_event) { color_scheme = color_scheme_event.getContent(); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 08dfe75584..625f4da98b 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -244,7 +244,7 @@ module.exports = React.createClass({ var room = MatrixClientPeg.get().getRoom(this.props.roomId); if (!room) return; - var color_scheme_event = room.getAccountData("m.room.color_scheme"); + var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); var color_scheme = {}; if (color_scheme_event) { color_scheme = color_scheme_event.getContent(); @@ -255,7 +255,7 @@ module.exports = React.createClass({ onRoomAccountData: function(room, event) { if (room.roomId == this.props.roomId) { - if (event.getType === "m.room.color_scheme") { + if (event.getType === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); @@ -810,7 +810,7 @@ module.exports = React.createClass({ if (new_color_scheme) { deferreds.push( MatrixClientPeg.get().setRoomAccountData( - this.state.room.roomId, "m.room.color_scheme", new_color_scheme + this.state.room.roomId, "org.matrix.room.color_scheme", new_color_scheme ) ); } diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 72d9a19bba..74ffa7f5a7 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -43,7 +43,7 @@ module.exports = React.createClass({ getInitialState: function() { // work out the initial color index var room_color_index = undefined; - var color_scheme_event = this.props.room.getAccountData("m.room.color_scheme"); + var color_scheme_event = this.props.room.getAccountData("org.matrix.room.color_scheme"); if (color_scheme_event) { var color_scheme = color_scheme_event.getContent(); if (color_scheme.primary_color) color_scheme.primary_color = color_scheme.primary_color.toLowerCase(); From c9c496f0e51a07b894f9a3ab06fd65a74d2b61a9 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Jan 2016 13:15:13 +0000 Subject: [PATCH 25/82] WIP all new roomsettings --- src/SlashCommands.js | 2 +- src/component-index.js | 1 + src/components/structures/MatrixChat.js | 2 +- src/components/structures/RoomView.js | 6 +- src/components/views/elements/EditableText.js | 8 +- src/components/views/messages/TextualBody.js | 3 + src/components/views/rooms/RoomSettings.js | 206 +++++++++++++----- 7 files changed, 161 insertions(+), 67 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 35216aa403..1dd7ecb08f 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -57,7 +57,7 @@ var commands = { } return success( MatrixClientPeg.get().setRoomAccountData( - room_id, "m.room.color_scheme", colorScheme + room_id, "org.matrix.room.color_scheme", colorScheme ) ); } diff --git a/src/component-index.js b/src/component-index.js index 0c08d70b73..7975b9a360 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -40,6 +40,7 @@ module.exports.components['views.dialogs.ErrorDialog'] = require('./components/v module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt'); module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog'); module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText'); +module.exports.components['views.elements.PowerSelector'] = require('./components/views/elements/PowerSelector'); module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar'); module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg'); module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector'); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 56885ce499..ef77787035 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -360,7 +360,7 @@ module.exports = React.createClass({ var theAlias = MatrixTools.getCanonicalAliasForRoom(room); if (theAlias) presentedId = theAlias; - var color_scheme_event = room.getAccountData("m.room.color_scheme"); + var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); var color_scheme = {}; if (color_scheme_event) { color_scheme = color_scheme_event.getContent(); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3851682b6f..9249a26351 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -244,7 +244,7 @@ module.exports = React.createClass({ var room = MatrixClientPeg.get().getRoom(this.props.roomId); if (!room) return; - var color_scheme_event = room.getAccountData("m.room.color_scheme"); + var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); var color_scheme = {}; if (color_scheme_event) { color_scheme = color_scheme_event.getContent(); @@ -255,7 +255,7 @@ module.exports = React.createClass({ onRoomAccountData: function(room, event) { if (room.roomId == this.props.roomId) { - if (event.getType === "m.room.color_scheme") { + if (event.getType === "org.matrix.room.color_scheme") { var color_scheme = event.getContent(); // XXX: we should validate the event Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); @@ -810,7 +810,7 @@ module.exports = React.createClass({ if (new_color_scheme) { deferreds.push( MatrixClientPeg.get().setRoomAccountData( - this.state.room.roomId, "m.room.color_scheme", new_color_scheme + this.state.room.roomId, "org.matrix.room.color_scheme", new_color_scheme ) ); } diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 6c6be899f5..ff41f26f42 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -33,6 +33,7 @@ module.exports = React.createClass({ labelClassName: React.PropTypes.string, placeholderClassName: React.PropTypes.string, blurToCancel: React.PropTypes.bool, + editable: React.PropTypes.bool, }, Phases: { @@ -49,6 +50,7 @@ module.exports = React.createClass({ initialValue: '', label: '', placeholder: '', + editable: true, }; }, @@ -141,6 +143,8 @@ module.exports = React.createClass({ }, onClickDiv: function(ev) { + if (!this.props.editable) return; + this.setState({ phase: this.Phases.Edit, }) @@ -178,9 +182,9 @@ module.exports = React.createClass({ render: function() { var editable_el; - if (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value) { + if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) { // show the label - editable_el =
{this.props.label}
; + editable_el =
{ this.props.label || this.props.initialValue }
; } else { // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together editable_el =
+

Directory

+
+ { alias_events.length ? "This room is accessible via:" : "This room has no aliases." } +
+
+ { alias_events.map(function(alias_event, i) { + return alias_event.getContent().aliases.map(function(alias, j) { + var deleteButton; + if (alias_event && alias_event.getStateKey() === domain) { + deleteButton = Delete; + } + return ( +
+ +
+ { deleteButton } +
+
+ ); + }); + })} + +
+ +
+ Add +
+
+
+
The canonical entry is  + +
+
; + var room_colors_section =

Room Colour

@@ -236,19 +316,19 @@ module.exports = React.createClass({
; } - var banned = this.props.room.getMembersWithMembership("ban"); - - var events_levels_section; - if (events_levels.length) { - events_levels_section = + var user_levels_section; + if (user_levels.length) { + user_levels_section =
-

Event levels

-
- {Object.keys(events_levels).map(function(event_type, i) { +
+ Users with specific roles are: +
+
+ {Object.keys(user_levels).map(function(user, i) { return ( -
- - +
+ { user } is a +
); })} @@ -256,6 +336,7 @@ module.exports = React.createClass({
; } + var banned = this.props.room.getMembersWithMembership("ban"); var banned_users_section; if (banned.length) { banned_users_section = @@ -273,69 +354,74 @@ module.exports = React.createClass({
; } + // TODO: support editing custom events_levels + // TODO: support editing custom user_levels + return (


+
{ room_colors_section } -

Power levels

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+ { aliases_section } -

User levels

-
- {Object.keys(user_levels).map(function(user, i) { +

Permissions

+
+
+ The default level for new room members is + +
+
+ To send messages, you must be a + +
+
+ To invite users into the room, you must be a + +
+
+ To configure the room (set room state), you must be a + +
+
+ To kick users, you must be a + +
+
+ To ban users, you must be a + +
+
+ To redact messages, you must be a + +
+ + {Object.keys(events_levels).map(function(event_type, i) { return ( -
- - +
+ To send events of type { event_type }, you must be a +
); })}
- { events_levels_section } +

Users

+
+
+ You are a +
+ + { user_levels_section } +
+ { banned_users_section } + { change_avatar } +
); } From 1b7d80a8cd2feaf6a3c9423d1c3d39742920597d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Jan 2016 14:04:00 +0000 Subject: [PATCH 26/82] s/getImplicitRoomName/getDefaultRoomName/ # as kegan doesn't like the word 'implicit' --- src/components/views/rooms/RoomHeader.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index be227bfe9d..0e69e24ede 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -54,7 +54,7 @@ module.exports = React.createClass({ this.setState({ name: name ? name.getContent().name : '', - implicitName: this.props.room.getImplicitRoomName(MatrixClientPeg.get().credentials.userId), + defaultName: this.props.room.getDefaultRoomName(MatrixClientPeg.get().credentials.userId), topic: topic ? topic.getContent().topic : '', }); } @@ -134,8 +134,8 @@ module.exports = React.createClass({ // if (topic) topic_el =
var placeholderName = "Unnamed Room"; - if (this.state.implicitName && this.state.implicitName !== '?') { - placeholderName += " (" + this.state.implicitName + ")"; + if (this.state.defaultName && this.state.defaultName !== '?') { + placeholderName += " (" + this.state.defaultName + ")"; } name = From 11025e2ba947f5cdc89d74e561eec17f990d1d82 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 13 Jan 2016 15:18:21 +0000 Subject: [PATCH 27/82] Make read marker ghost same width as normal one. --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ddde9c2645..8e193b25ab 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -839,7 +839,7 @@ module.exports = React.createClass({ // is the last element or not, because we only decide as we're going along. if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) { var hr; - hr = (
Date: Wed, 13 Jan 2016 15:55:28 +0000 Subject: [PATCH 28/82] Factor out presence text. Do prep work for displaying 3pid invites on memberlist. Factored out presence to PresenceLabel. --- src/component-index.js | 1 + src/components/views/rooms/MemberList.js | 30 +++++++- src/components/views/rooms/MemberTile.js | 51 +++---------- src/components/views/rooms/PresenceLabel.js | 84 +++++++++++++++++++++ 4 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 src/components/views/rooms/PresenceLabel.js diff --git a/src/component-index.js b/src/component-index.js index 9fe15adfc6..7ae15ba12c 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -64,6 +64,7 @@ module.exports.components['views.rooms.MemberInfo'] = require('./components/view module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer'); +module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel'); module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader'); module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList'); module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar'); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index b0e2baa3d3..f4d017733d 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -229,7 +229,8 @@ module.exports = React.createClass({ var MemberTile = sdk.getComponent("rooms.MemberTile"); var self = this; - return self.state.members.filter(function(userId) { + + var memberList = self.state.members.filter(function(userId) { var m = self.memberDict[userId]; return m.membership == membership; }).map(function(userId) { @@ -238,6 +239,33 @@ module.exports = React.createClass({ ); }); + + if (membership === "invite") { + // include 3pid invites (m.room.third_party_invite) state events. + // The HS may have already converted these into m.room.member invites so + // we shouldn't add them if the 3pid invite state key (token) is in the + // member invite (content.third_party_invite.signed.token) + var room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (room) { + room.currentState.getStateEvents("m.room.third_party_invite").forEach( + function(e) { + // discard all invites which have a m.room.member event since we've + // already added them. + var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()); + if (memberEvent) { + console.log("Found match => %s", memberEvent.getStateKey()); + return; + } + console.log("Display match => "); + /* + memberList.push( + + ) */ + }) + } + } + + return memberList; }, onPopulateInvite: function(e) { diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 32cc619f13..0a32441a00 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -26,6 +26,11 @@ var Modal = require("../../../Modal"); module.exports = React.createClass({ displayName: 'MemberTile', + propTypes: { + member: React.PropTypes.any.isRequired, // RoomMember + onFinished: React.PropTypes.func + }, + getInitialState: function() { return {}; }, @@ -71,37 +76,6 @@ module.exports = React.createClass({ }); }, - 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) { @@ -111,6 +85,8 @@ module.exports = React.createClass({ }, render: function() { + var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); + this.member_last_modified_time = this.props.member.getLastModifiedTime(); if (this.props.member.user) { this.user_last_modified_time = this.props.member.user.getLastModifiedTime(); @@ -144,22 +120,17 @@ module.exports = React.createClass({ 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 = + nameEl = (
{ name }
- { presence } +
+ ); } else { nameEl = diff --git a/src/components/views/rooms/PresenceLabel.js b/src/components/views/rooms/PresenceLabel.js new file mode 100644 index 0000000000..4ecad5b3df --- /dev/null +++ b/src/components/views/rooms/PresenceLabel.js @@ -0,0 +1,84 @@ +/* +Copyright 2015, 2016 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'); + +module.exports = React.createClass({ + displayName: 'PresenceLabel', + + propTypes: { + activeAgo: React.PropTypes.number, + presenceState: React.PropTypes.string + }, + + getDefaultProps: function() { + return { + ago: -1, + presenceState: null + }; + }, + + 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(presence) { + if (presence === "online") return "Online"; + if (presence === "unavailable") return "Idle"; // XXX: is this actually right? + if (presence === "offline") return "Offline"; + return "Unknown"; + }, + + render: function() { + if (this.props.activeAgo >= 0) { + return ( +
+ { this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago +
+ ); + } + else { + return ( +
+ { this.getPrettyPresence(this.props.presenceState) } +
+ ); + } + } +}); From 8c9352c484ef8e367dce0837686c492bb61acc51 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 13 Jan 2016 16:55:28 +0000 Subject: [PATCH 29/82] Make MemberAvatar and MemberTile work without RoomMember objects --- src/components/views/avatars/MemberAvatar.js | 71 ++++++++------- src/components/views/rooms/MemberList.js | 9 +- src/components/views/rooms/MemberTile.js | 96 +++++++++++--------- 3 files changed, 98 insertions(+), 78 deletions(-) diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 21c717aac5..f209006b1c 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -24,10 +24,16 @@ module.exports = React.createClass({ displayName: 'MemberAvatar', propTypes: { - member: React.PropTypes.object.isRequired, + member: React.PropTypes.object, width: React.PropTypes.number, height: React.PropTypes.number, resizeMethod: React.PropTypes.string, + /** + * The custom display name to use for this member. This can serve as a + * drop in replacement for RoomMember objects, or as a clobber name on + * an existing RoomMember. Used for 3pid invites. + */ + customDisplayName: React.PropTypes.string }, getDefaultProps: function() { @@ -38,64 +44,68 @@ module.exports = React.createClass({ } }, + getInitialState: function() { + var defaultImageUrl = Avatar.defaultAvatarUrlForString( + this.props.customDisplayName || this.props.member.userId + ) + return { + imageUrl: this._getMemberImageUrl() || defaultImageUrl, + defaultImageUrl: defaultImageUrl + }; + }, + componentWillReceiveProps: function(nextProps) { this.refreshUrl(); }, - defaultAvatarUrl: function(member, width, height, resizeMethod) { - return Avatar.defaultAvatarUrlForString(member.userId); - }, - onError: function(ev) { // don't tightloop if the browser can't load a data url - if (ev.target.src == this.defaultAvatarUrl(this.props.member)) { + if (ev.target.src == this.state.defaultImageUrl) { return; } this.setState({ - imageUrl: this.defaultAvatarUrl(this.props.member) + imageUrl: this.state.defaultImageUrl }); }, - _computeUrl: function() { + _getMemberImageUrl: function() { + if (!this.props.member) { return null; } + return Avatar.avatarUrlForMember(this.props.member, this.props.width, this.props.height, this.props.resizeMethod); }, + _getInitialLetter: function() { + var name = this.props.customDisplayName || this.props.member.name; + var initial = name[0]; + if (initial === '@' && name[1]) { + initial = name[1]; + } + return initial.toUpperCase(); + }, + refreshUrl: function() { - var newUrl = this._computeUrl(); + var newUrl = this._getMemberImageUrl(); if (newUrl != this.currentUrl) { this.currentUrl = newUrl; this.setState({imageUrl: newUrl}); } }, - getInitialState: function() { - return { - imageUrl: this._computeUrl() - }; - }, - - - /////////////// - render: function() { - // XXX: recalculates default avatar url constantly - if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) { - var initial; - if (this.props.member.name[0]) - initial = this.props.member.name[0].toUpperCase(); - if (initial === '@' && this.props.member.name[1]) - initial = this.props.member.name[1].toUpperCase(); - + var name = this.props.customDisplayName || this.props.member.name; + + if (this.state.imageUrl === this.state.defaultImageUrl) { + var initialLetter = this._getInitialLetter(); return ( - { initialLetter } + ); @@ -104,9 +114,8 @@ module.exports = React.createClass({ + title={name} + {...this.props} /> ); } }); diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index f4d017733d..eac5466e88 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -15,6 +15,7 @@ limitations under the License. */ var React = require('react'); var classNames = require('classnames'); +var Matrix = require("matrix-js-sdk"); var MatrixClientPeg = require("../../../MatrixClientPeg"); var Modal = require("../../../Modal"); var sdk = require('../../../index'); @@ -253,14 +254,12 @@ module.exports = React.createClass({ // already added them. var memberEvent = room.currentState.getInviteForThreePidToken(e.getStateKey()); if (memberEvent) { - console.log("Found match => %s", memberEvent.getStateKey()); return; } - console.log("Display match => "); - /* memberList.push( - - ) */ + + ) }) } } diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 0a32441a00..4752c4d539 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -27,24 +27,18 @@ module.exports = React.createClass({ displayName: 'MemberTile', propTypes: { - member: React.PropTypes.any.isRequired, // RoomMember - onFinished: React.PropTypes.func + member: React.PropTypes.any, // RoomMember + onFinished: React.PropTypes.func, + customDisplayName: React.PropTypes.string // for 3pid invites }, getInitialState: function() { return {}; }, - onLeaveClick: function() { - dis.dispatch({ - action: 'leave_room', - room_id: this.props.member.roomId, - }); - this.props.onFinished(); - }, - shouldComponentUpdate: function(nextProps, nextState) { if (this.state.hover !== nextState.hover) return true; + if (!this.props.member) { return false; } // e.g. 3pid members if ( this.member_last_modified_time === undefined || this.member_last_modified_time < nextProps.member.getLastModifiedTime() @@ -70,13 +64,25 @@ module.exports = React.createClass({ }, onClick: function(e) { + if (!this.props.member) { return; } // e.g. 3pid members + dis.dispatch({ action: 'view_user', member: this.props.member, }); }, + _getDisplayName: function() { + if (this.props.customDisplayName) { + return this.props.customDisplayName; + } + return this.props.member.name; + }, + getPowerLabel: function() { + if (!this.props.member) { + return this._getDisplayName(); + } var label = this.props.member.userId; if (this.state.isTargetMod) { label += " - Mod (" + this.props.member.powerLevelNorm + "%)"; @@ -85,68 +91,74 @@ module.exports = React.createClass({ }, render: function() { - var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); - - 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 member = this.props.member; + var isMyUser = false; + var name = this._getDisplayName(); + var active = -1; 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"; + + if (member) { + if (member.user) { + this.user_last_modified_time = member.user.getLastModifiedTime(); + + // FIXME: make presence data update whenever User.presence changes... + active = ( + (Date.now() - (member.user.lastPresenceTs - member.user.lastActiveAgo)) || -1 + ); + + if (member.user.presence === "online") { + presenceClass = "mx_MemberTile_online"; + } + else if (member.user.presence === "unavailable") { + presenceClass = "mx_MemberTile_unavailable"; + } } + this.member_last_modified_time = member.getLastModifiedTime(); + isMyUser = MatrixClientPeg.get().credentials.userId == member.userId; + + // 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 mainClassName = "mx_MemberTile "; 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 nameEl; if (this.state.hover) { - // 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; - + var presenceState = (member && member.user) ? member.user.presence : null; + var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); nameEl = (
{ name }
+ presenceState={presenceState} />
); } else { - nameEl = + nameEl = (
{ name }
+ ); } var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + return (
- - { power } +
{ nameEl }
From 53f31e49dad440ae3bf87d6511d02c73139ecd05 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 13 Jan 2016 17:46:36 +0000 Subject: [PATCH 30/82] Implement tab-complete for slash commands This needed a new interface function `getOverrideSuffix()` so we didn't suffix commands at the start with ": ". All seems to work. --- src/SlashCommands.js | 6 +++++ src/TabComplete.js | 33 +++++++++++++++------------ src/TabCompleteEntries.js | 29 +++++++++++++++++++++++ src/components/structures/RoomView.js | 6 ++++- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 1dd7ecb08f..3de3943b9e 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -322,5 +322,11 @@ module.exports = { } } return null; // not a command + }, + + getCommandList: function() { + return Object.keys(commands).map(function(cmd) { + return "/" + cmd; + }); } }; diff --git a/src/TabComplete.js b/src/TabComplete.js index 6690802d5d..3b117ca689 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -58,7 +58,7 @@ class TabComplete { // assign onClick listeners for each entry to complete the text this.list.forEach((l) => { l.onClick = () => { - this.completeTo(l.getText()); + this.completeTo(l); } }); } @@ -93,10 +93,10 @@ class TabComplete { /** * Do an auto-complete with the given word. This terminates the tab-complete. - * @param {string} someVal + * @param {Entry} entry The tab-complete entry to complete to. */ - completeTo(someVal) { - this.textArea.value = this._replaceWith(someVal, true); + completeTo(entry) { + this.textArea.value = this._replaceWith(entry.getText(), true, entry.getOverrideSuffix()); this.stopTabCompleting(); // keep focus on the text area this.textArea.focus(); @@ -222,8 +222,9 @@ class TabComplete { if (!this.inPassiveMode) { // set textarea to this new value this.textArea.value = this._replaceWith( - this.matchedList[this.currentIndex].text, - this.currentIndex !== 0 // don't suffix the original text! + this.matchedList[this.currentIndex].getText(), + this.currentIndex !== 0, // don't suffix the original text! + this.matchedList[this.currentIndex].getOverrideSuffix() ); } @@ -243,7 +244,7 @@ class TabComplete { } } - _replaceWith(newVal, includeSuffix) { + _replaceWith(newVal, includeSuffix, overrideSuffix) { // The regex to replace the input matches a character of whitespace AND // the partial word. If we just use string.replace() with the regex it will // replace the partial word AND the character of whitespace. We want to @@ -258,13 +259,17 @@ class TabComplete { boundaryChar = ""; } - var replacementText = ( - boundaryChar + newVal + ( - includeSuffix ? - (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix) : - "" - ) - ); + var suffix = ""; + if (includeSuffix) { + if (overrideSuffix) { + suffix = overrideSuffix; + } + else { + suffix = (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix); + } + } + + var replacementText = boundaryChar + newVal + suffix; return this.originalText.replace(MATCH_REGEX, function() { return replacementText; // function form to avoid `$` special-casing }); diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index d3efc0d2f1..4b7fbc5d0e 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -42,6 +42,14 @@ class Entry { return null; } + /** + * @return {?string} The suffix to override whatever the default is, or null to + * not do this. + */ + getOverrideSuffix() { + return null; + } + /** * Called when this entry is clicked. */ @@ -50,6 +58,26 @@ class Entry { } } +class CommandEntry extends Entry { + constructor(command) { + super(command); + } + + getKey() { + return this.getText(); + } + + getOverrideSuffix() { + return " "; // force a space after the command. + } +} + +CommandEntry.fromStrings = function(commandArray) { + return commandArray.map(function(cmd) { + return new CommandEntry(cmd); + }); +} + class MemberEntry extends Entry { constructor(member) { super(member.name || member.userId); @@ -99,3 +127,4 @@ MemberEntry.fromMemberList = function(members) { module.exports.Entry = Entry; module.exports.MemberEntry = MemberEntry; +module.exports.CommandEntry = CommandEntry; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8e193b25ab..bc6438a97c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -35,7 +35,9 @@ var sdk = require('../../index'); var CallHandler = require('../../CallHandler'); var TabComplete = require("../../TabComplete"); var MemberEntry = require("../../TabCompleteEntries").MemberEntry; +var CommandEntry = require("../../TabCompleteEntries").CommandEntry; var Resend = require("../../Resend"); +var SlashCommands = require("../../SlashCommands"); var dis = require("../../dispatcher"); var Tinter = require("../../Tinter"); @@ -416,7 +418,9 @@ module.exports = React.createClass({ return; } this.tabComplete.setCompletionList( - MemberEntry.fromMemberList(room.getJoinedMembers()) + MemberEntry.fromMemberList(room.getJoinedMembers()).concat( + CommandEntry.fromStrings(SlashCommands.getCommandList()) + ) ); }, From 05c789187444d0f8d594df106da46435409d3f08 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Jan 2016 17:54:33 +0000 Subject: [PATCH 31/82] fix NPE --- src/components/views/rooms/RoomSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index f3b3b356df..9324824087 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -86,7 +86,7 @@ module.exports = React.createClass({ }, getTopic: function() { - return this.refs.topic.value; + return this.refs.topic ? this.refs.topic.value : ""; }, getJoinRules: function() { From 123b134d87b54da0cd70bd1fec52d22c577d85a6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 13 Jan 2016 18:15:59 +0000 Subject: [PATCH 32/82] use getDomain() --- src/SlashCommands.js | 6 ++---- src/components/structures/CreateRoom.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 3de3943b9e..ba4b08c3d0 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -117,8 +117,7 @@ var commands = { return reject("Usage: /join #alias:domain"); } if (!room_alias.match(/:/)) { - var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); - room_alias += ':' + domain; + room_alias += ':' + MatrixClientPeg.get().getDomain(); } // Try to find a room with this alias @@ -164,8 +163,7 @@ var commands = { return reject("Usage: /part [#alias:domain]"); } if (!room_alias.match(/:/)) { - var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); - room_alias += ':' + domain; + room_alias += ':' + MatrixClientPeg.get().getDomain(); } // Try to find a room with this alias diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index c21bc80c6b..116202d324 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -251,7 +251,7 @@ module.exports = React.createClass({ var UserSelector = sdk.getComponent("elements.UserSelector"); var RoomHeader = sdk.getComponent("rooms.RoomHeader"); - var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); + var domain = MatrixClientPeg.get().getDomain(); return (
From 864d10f4124fd62bd462c24f88e8cdf078dc0522 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 11:39:24 +0000 Subject: [PATCH 33/82] Make individual Entrys responsible for determining suffixes This makes it cleaner as CommandEntry always wants a space, but MemberEntry wants a space or ": " depending on if it is the first word or not. --- src/TabComplete.js | 21 ++++++++------------- src/TabCompleteEntries.js | 10 +++++++--- src/components/structures/RoomView.js | 2 -- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/TabComplete.js b/src/TabComplete.js index 3b117ca689..59f3cec3a0 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -32,8 +32,6 @@ const MATCH_REGEX = /(^|\s)(\S+)$/; class TabComplete { constructor(opts) { - opts.startingWordSuffix = opts.startingWordSuffix || ""; - opts.wordSuffix = opts.wordSuffix || ""; opts.allowLooping = opts.allowLooping || false; opts.autoEnterTabComplete = opts.autoEnterTabComplete || false; opts.onClickCompletes = opts.onClickCompletes || false; @@ -96,7 +94,9 @@ class TabComplete { * @param {Entry} entry The tab-complete entry to complete to. */ completeTo(entry) { - this.textArea.value = this._replaceWith(entry.getText(), true, entry.getOverrideSuffix()); + this.textArea.value = this._replaceWith( + entry.getText(), true, entry.getSuffix(this.isFirstWord) + ); this.stopTabCompleting(); // keep focus on the text area this.textArea.focus(); @@ -224,7 +224,7 @@ class TabComplete { this.textArea.value = this._replaceWith( this.matchedList[this.currentIndex].getText(), this.currentIndex !== 0, // don't suffix the original text! - this.matchedList[this.currentIndex].getOverrideSuffix() + this.matchedList[this.currentIndex].getSuffix(this.isFirstWord) ); } @@ -244,7 +244,7 @@ class TabComplete { } } - _replaceWith(newVal, includeSuffix, overrideSuffix) { + _replaceWith(newVal, includeSuffix, suffix) { // The regex to replace the input matches a character of whitespace AND // the partial word. If we just use string.replace() with the regex it will // replace the partial word AND the character of whitespace. We want to @@ -259,14 +259,9 @@ class TabComplete { boundaryChar = ""; } - var suffix = ""; - if (includeSuffix) { - if (overrideSuffix) { - suffix = overrideSuffix; - } - else { - suffix = (this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix); - } + suffix = suffix || ""; + if (!includeSuffix) { + suffix = ""; } var replacementText = boundaryChar + newVal + suffix; diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 4b7fbc5d0e..79e0a9a46b 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -43,10 +43,10 @@ class Entry { } /** - * @return {?string} The suffix to override whatever the default is, or null to + * @return {?string} The suffix to append to the tab-complete, or null to * not do this. */ - getOverrideSuffix() { + getSuffix(isFirstWord) { return null; } @@ -67,7 +67,7 @@ class CommandEntry extends Entry { return this.getText(); } - getOverrideSuffix() { + getSuffix(isFirstWord) { return " "; // force a space after the command. } } @@ -94,6 +94,10 @@ class MemberEntry extends Entry { getKey() { return this.member.userId; } + + getSuffix(isFirstWord) { + return isFirstWord ? ": " : " "; + } } MemberEntry.fromMemberList = function(members) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index bc6438a97c..abfa2e27f1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -96,8 +96,6 @@ module.exports = React.createClass({ // xchat-style tab complete, add a colon if tab // completing at the start of the text this.tabComplete = new TabComplete({ - startingWordSuffix: ": ", - wordSuffix: " ", allowLooping: false, autoEnterTabComplete: true, onClickCompletes: true, From b67131f0c83fd5da063d77512e784008b02509b7 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 14:39:58 +0000 Subject: [PATCH 34/82] Add a Command class; add Entry.getFillText() getFillText() serves to decouple the text displayed in the auto-complete list via getText() and the text actually filled into the box via getFillText(). This allows us to display command + args on the list but only fill the command part. A Command class has been added to provide some structure when extracting the command name and args. Manually tested and it works. --- src/SlashCommands.js | 104 +++++++++++------- src/TabComplete.js | 4 +- src/TabCompleteEntries.js | 23 +++- src/components/structures/RoomView.js | 2 +- src/components/views/rooms/MessageComposer.js | 2 +- 5 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 3de3943b9e..83b4b52ffc 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -20,6 +20,31 @@ var dis = require("./dispatcher"); var encryption = require("./encryption"); var Tinter = require("./Tinter"); + +class Command { + constructor(name, paramArgs, runFn) { + this.name = name; + this.paramArgs = paramArgs; + this.runFn = runFn; + } + + getCommand() { + return "/" + this.name; + } + + getCommandWithArgs() { + return this.getCommand() + " " + this.paramArgs; + } + + run(roomId, args) { + return this.runFn.bind(this)(roomId, args); + } + + getUsage() { + return "Usage: " + this.getCommandWithArgs() + } +} + var reject = function(msg) { return { error: msg @@ -34,18 +59,17 @@ var success = function(promise) { var commands = { // Change your nickname - nick: function(room_id, args) { + nick: new Command("nick", "", function(room_id, args) { if (args) { return success( MatrixClientPeg.get().setDisplayName(args) ); } - return reject("Usage: /nick "); - }, + return reject(this.getUsage()); + }), // Changes the colorscheme of your current room - tint: function(room_id, args) { - + tint: new Command("tint", " []", function(room_id, args) { if (args) { var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { @@ -62,10 +86,10 @@ var commands = { ); } } - return reject("Usage: /tint []"); - }, + return reject(this.getUsage()); + }), - encrypt: function(room_id, args) { + encrypt: new Command("encrypt", "", function(room_id, args) { if (args == "on") { var client = MatrixClientPeg.get(); var members = client.getRoom(room_id).currentState.members; @@ -81,21 +105,21 @@ var commands = { ); } - return reject("Usage: encrypt "); - }, + return reject(this.getUsage()); + }), // Change the room topic - topic: function(room_id, args) { + topic: new Command("topic", "", function(room_id, args) { if (args) { return success( MatrixClientPeg.get().setRoomTopic(room_id, args) ); } - return reject("Usage: /topic "); - }, + return reject(this.getUsage()); + }), // Invite a user - invite: function(room_id, args) { + invite: new Command("invite", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -104,11 +128,11 @@ var commands = { ); } } - return reject("Usage: /invite "); - }, + return reject(this.getUsage()); + }), // Join a room - join: function(room_id, args) { + join: new Command("join", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -151,17 +175,17 @@ var commands = { ); } } - return reject("Usage: /join "); - }, + return reject(this.getUsage()); + }), - part: function(room_id, args) { + part: new Command("part", "[#alias:domain]", function(room_id, args) { var targetRoomId; if (args) { var matches = args.match(/^(\S+)$/); if (matches) { var room_alias = matches[1]; if (room_alias[0] !== '#') { - return reject("Usage: /part [#alias:domain]"); + return reject(this.getUsage()); } if (!room_alias.match(/:/)) { var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, ''); @@ -198,10 +222,10 @@ var commands = { dis.dispatch({action: 'view_next_room'}); }) ); - }, + }), // Kick a user from the room with an optional reason - kick: function(room_id, args) { + kick: new Command("kick", " []", function(room_id, args) { if (args) { var matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { @@ -210,11 +234,11 @@ var commands = { ); } } - return reject("Usage: /kick []"); - }, + return reject(this.getUsage()); + }), // Ban a user from the room with an optional reason - ban: function(room_id, args) { + ban: new Command("ban", " []", function(room_id, args) { if (args) { var matches = args.match(/^(\S+?)( +(.*))?$/); if (matches) { @@ -223,11 +247,11 @@ var commands = { ); } } - return reject("Usage: /ban []"); - }, + return reject(this.getUsage()); + }), // Unban a user from the room - unban: function(room_id, args) { + unban: new Command("unban", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -237,11 +261,11 @@ var commands = { ); } } - return reject("Usage: /unban "); - }, + return reject(this.getUsage()); + }), // Define the power level of a user - op: function(room_id, args) { + op: new Command("op", " []", function(room_id, args) { if (args) { var matches = args.match(/^(\S+?)( +(\d+))?$/); var powerLevel = 50; // default power level for op @@ -266,11 +290,11 @@ var commands = { } } } - return reject("Usage: /op []"); - }, + return reject(this.getUsage()); + }), // Reset the power level of a user - deop: function(room_id, args) { + deop: new Command("deop", "", function(room_id, args) { if (args) { var matches = args.match(/^(\S+)$/); if (matches) { @@ -289,8 +313,8 @@ var commands = { ); } } - return reject("Usage: /deop "); - } + return reject(this.getUsage()); + }) }; // helpful aliases @@ -315,7 +339,7 @@ module.exports = { var args = bits[3]; if (cmd === "me") return null; if (commands[cmd]) { - return commands[cmd](roomId, args); + return commands[cmd].run(roomId, args); } else { return reject("Unrecognised command: " + input); @@ -325,8 +349,8 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).map(function(cmd) { - return "/" + cmd; + return Object.keys(commands).map(function(cmdKey) { + return commands[cmdKey]; }); } }; diff --git a/src/TabComplete.js b/src/TabComplete.js index 59f3cec3a0..8886e21af9 100644 --- a/src/TabComplete.js +++ b/src/TabComplete.js @@ -95,7 +95,7 @@ class TabComplete { */ completeTo(entry) { this.textArea.value = this._replaceWith( - entry.getText(), true, entry.getSuffix(this.isFirstWord) + entry.getFillText(), true, entry.getSuffix(this.isFirstWord) ); this.stopTabCompleting(); // keep focus on the text area @@ -222,7 +222,7 @@ class TabComplete { if (!this.inPassiveMode) { // set textarea to this new value this.textArea.value = this._replaceWith( - this.matchedList[this.currentIndex].getText(), + this.matchedList[this.currentIndex].getFillText(), this.currentIndex !== 0, // don't suffix the original text! this.matchedList[this.currentIndex].getSuffix(this.isFirstWord) ); diff --git a/src/TabCompleteEntries.js b/src/TabCompleteEntries.js index 79e0a9a46b..9aef7736a8 100644 --- a/src/TabCompleteEntries.js +++ b/src/TabCompleteEntries.js @@ -28,6 +28,14 @@ class Entry { return this.text; } + /** + * @return {string} The text to insert into the input box. Most of the time + * this is the same as getText(). + */ + getFillText() { + return this.text; + } + /** * @return {ReactClass} Raw JSX */ @@ -59,12 +67,17 @@ class Entry { } class CommandEntry extends Entry { - constructor(command) { - super(command); + constructor(cmd, cmdWithArgs) { + super(cmdWithArgs); + this.cmd = cmd; + } + + getFillText() { + return this.cmd; } getKey() { - return this.getText(); + return this.getFillText(); } getSuffix(isFirstWord) { @@ -72,9 +85,9 @@ class CommandEntry extends Entry { } } -CommandEntry.fromStrings = function(commandArray) { +CommandEntry.fromCommands = function(commandArray) { return commandArray.map(function(cmd) { - return new CommandEntry(cmd); + return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs()); }); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index abfa2e27f1..6f5f9a97a3 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -417,7 +417,7 @@ module.exports = React.createClass({ } this.tabComplete.setCompletionList( MemberEntry.fromMemberList(room.getJoinedMembers()).concat( - CommandEntry.fromStrings(SlashCommands.getCommandList()) + CommandEntry.fromCommands(SlashCommands.getCommandList()) ) ); }, diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index a3ad033acc..930725570b 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -341,7 +341,7 @@ module.exports = React.createClass({ MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText); } - sendMessagePromise.then(function() { + sendMessagePromise.done(function() { dis.dispatch({ action: 'message_sent' }); From 7e5d4b8ce8d50e67df0a6a242d4f8f94a216d4ce Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 16:01:31 +0000 Subject: [PATCH 35/82] Send an event at the end of user activity too and use this to send RRs. --- src/UserActivity.js | 32 ++++++++++++++++++++++++++- src/components/structures/RoomView.js | 6 +++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/UserActivity.js b/src/UserActivity.js index 8b136c0bcc..2e485ed58c 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -16,7 +16,8 @@ limitations under the License. var dis = require("./dispatcher"); -var MIN_DISPATCH_INTERVAL = 1 * 1000; +var MIN_DISPATCH_INTERVAL = 500; +var CURRENTLY_ACTIVE_THRESHOLD = 500; /** * This class watches for user activity (moving the mouse or pressing a key) @@ -38,6 +39,7 @@ class UserActivity { window.addEventListener('wheel', this._onUserActivity.bind(this), true); this.lastActivityAtTs = new Date().getTime(); this.lastDispatchAtTs = 0; + this.activityEndTimer = undefined; } /** @@ -49,6 +51,14 @@ class UserActivity { window.removeEventListener('wheel', this._onUserActivity.bind(this), true); } + /** + * Return true if there has been user activity very recently + * (ie. within a few seconds) + */ + userCurrentlyActive() { + return this.lastActivityAtTs > (new Date).getTime() - CURRENTLY_ACTIVE_THRESHOLD; + } + _onUserActivity(event) { if (event.screenX && event.type == "mousemove") { if (event.screenX === this.lastScreenX && @@ -67,6 +77,26 @@ class UserActivity { dis.dispatch({ action: 'user_activity' }); + if (!this.activityEndTimer) { + this.activityEndTimer = setTimeout( + this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL + ); + } + } + } + + _onActivityEndTimer() { + var now = (new Date).getTime(); + var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL; + if (now >= targetTime) { + dis.dispatch({ + action: 'user_activity_end' + }); + this.activityEndTimer = undefined; + } else { + this.activityEndTimer = setTimeout( + this._onActivityEndTimer.bind(this), targetTime - now + ); } } } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8e193b25ab..842bb1273d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -220,6 +220,12 @@ module.exports = React.createClass({ break; case 'user_activity': + case 'user_activity_end': + // we could treat user_activity_end differently and not + // send receipts for messages that have arrived between + // the actual user activity and the time they stopped + // being active, but let's see if this is actually + // necessary. this.sendReadReceipt(); break; } From 4f21e2beb3e64901de02e369d07a6c45b15aa458 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 16:13:50 +0000 Subject: [PATCH 36/82] Suffix with units --- src/UserActivity.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/UserActivity.js b/src/UserActivity.js index 2e485ed58c..5ed94a5f36 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -16,8 +16,8 @@ limitations under the License. var dis = require("./dispatcher"); -var MIN_DISPATCH_INTERVAL = 500; -var CURRENTLY_ACTIVE_THRESHOLD = 500; +var MIN_DISPATCH_INTERVAL_MS = 500; +var CURRENTLY_ACTIVE_THRESHOLD_MS = 500; /** * This class watches for user activity (moving the mouse or pressing a key) @@ -56,7 +56,7 @@ class UserActivity { * (ie. within a few seconds) */ userCurrentlyActive() { - return this.lastActivityAtTs > (new Date).getTime() - CURRENTLY_ACTIVE_THRESHOLD; + return this.lastActivityAtTs > (new Date).getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS; } _onUserActivity(event) { @@ -72,14 +72,14 @@ class UserActivity { } this.lastActivityAtTs = (new Date).getTime(); - if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) { + if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) { this.lastDispatchAtTs = this.lastActivityAtTs; dis.dispatch({ action: 'user_activity' }); if (!this.activityEndTimer) { this.activityEndTimer = setTimeout( - this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL + this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS ); } } @@ -87,7 +87,7 @@ class UserActivity { _onActivityEndTimer() { var now = (new Date).getTime(); - var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL; + var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS; if (now >= targetTime) { dis.dispatch({ action: 'user_activity_end' From 740c22238eb51b1c9a5140a8cb7084ad063e7f05 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 16:15:07 +0000 Subject: [PATCH 37/82] Better date syntax --- src/UserActivity.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UserActivity.js b/src/UserActivity.js index 5ed94a5f36..669b007934 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -56,7 +56,7 @@ class UserActivity { * (ie. within a few seconds) */ userCurrentlyActive() { - return this.lastActivityAtTs > (new Date).getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS; + return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS; } _onUserActivity(event) { @@ -71,7 +71,7 @@ class UserActivity { this.lastScreenY = event.screenY; } - this.lastActivityAtTs = (new Date).getTime(); + this.lastActivityAtTs = new Date().getTime(); if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) { this.lastDispatchAtTs = this.lastActivityAtTs; dis.dispatch({ @@ -86,7 +86,7 @@ class UserActivity { } _onActivityEndTimer() { - var now = (new Date).getTime(); + var now = new Date().getTime(); var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS; if (now >= targetTime) { dis.dispatch({ From 84a7fc16401eea018b74a8ee0ad6452c72bfc394 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 16:29:01 +0000 Subject: [PATCH 38/82] Tweak how command aliases are set This prevents multiple commands of the same name being returned in getCommandList() --- src/SlashCommands.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 83b4b52ffc..938bf062cb 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -318,7 +318,9 @@ var commands = { }; // helpful aliases -commands.j = commands.join; +var aliases = { + j: "join" +} module.exports = { /** @@ -338,6 +340,9 @@ module.exports = { var cmd = bits[1].substring(1).toLowerCase(); var args = bits[3]; if (cmd === "me") return null; + if (aliases[cmd]) { + cmd = aliases[cmd]; + } if (commands[cmd]) { return commands[cmd].run(roomId, args); } From 42dc1be3410ee42466064b44e2f1286d53603757 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 16:29:30 +0000 Subject: [PATCH 39/82] fix descriptions a bit and sort the slash commands when tab-completing --- src/SlashCommands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 938bf062cb..ca3a010791 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -69,7 +69,7 @@ var commands = { }), // Changes the colorscheme of your current room - tint: new Command("tint", " []", function(room_id, args) { + tint: new Command("tint", " []", function(room_id, args) { if (args) { var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); if (matches) { @@ -89,7 +89,7 @@ var commands = { return reject(this.getUsage()); }), - encrypt: new Command("encrypt", "", function(room_id, args) { + encrypt: new Command("encrypt", "", function(room_id, args) { if (args == "on") { var client = MatrixClientPeg.get(); var members = client.getRoom(room_id).currentState.members; @@ -354,7 +354,7 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).map(function(cmdKey) { + return Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; }); } From 4430e16707e669eb7d29beb629cdc0d74f25ba50 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 16:29:41 +0000 Subject: [PATCH 40/82] apply CSS to slashcommand autocompletes --- src/components/views/rooms/TabCompleteBar.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/TabCompleteBar.js b/src/components/views/rooms/TabCompleteBar.js index c640d6aa5b..ea74706f29 100644 --- a/src/components/views/rooms/TabCompleteBar.js +++ b/src/components/views/rooms/TabCompleteBar.js @@ -18,6 +18,7 @@ limitations under the License. var React = require('react'); var MatrixClientPeg = require("../../../MatrixClientPeg"); +var CommandEntry = require("../../../TabCompleteEntries").CommandEntry; module.exports = React.createClass({ displayName: 'TabCompleteBar', @@ -31,8 +32,9 @@ module.exports = React.createClass({
{this.props.entries.map(function(entry, i) { return ( -
+
{entry.getImageJsx()} {entry.getText()} From d33d60691203efe54b7ff903db5776174c7d70c2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 16:31:42 +0000 Subject: [PATCH 41/82] Only show uploads that are going to the current room Fixes #506 --- src/components/structures/UploadBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index 12e502026f..eda843eb8a 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar', } } if (!upload) { - upload = uploads[0]; + return
} var innerProgressStyle = { From d767e724696a66317fcef6273d25cbb42b6f18c5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 17:02:56 +0000 Subject: [PATCH 42/82] hide hoverover for 3pids --- src/components/views/rooms/MemberTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberTile.js b/src/components/views/rooms/MemberTile.js index 4752c4d539..58d9d59276 100644 --- a/src/components/views/rooms/MemberTile.js +++ b/src/components/views/rooms/MemberTile.js @@ -130,7 +130,7 @@ module.exports = React.createClass({ } var nameEl; - if (this.state.hover) { + if (this.state.hover && this.props.member) { var presenceState = (member && member.user) ? member.user.presence : null; var PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); nameEl = ( From 2899082cbaa3b34cd26426d522914727e8bac4a3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 17:24:52 +0000 Subject: [PATCH 43/82] deselect editabletext when tabbing away --- src/components/views/elements/EditableText.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index ff41f26f42..78d9d3d0c2 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -173,6 +173,9 @@ module.exports = React.createClass({ }, onBlur: function(ev) { + var sel = window.getSelection(); + sel.removeAllRanges(); + if (this.props.blurToCancel) this.cancelEdit(); else From e4671205d86ffc427c1b1a752322695a2e934830 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 14 Jan 2016 17:24:57 +0000 Subject: [PATCH 44/82] right imagery --- src/components/views/rooms/RoomSettings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index e74df23540..0bb27658ff 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -248,7 +248,7 @@ module.exports = React.createClass({ return alias_event.getContent().aliases.map(function(alias, j) { var deleteButton; if (alias_event && alias_event.getStateKey() === domain) { - deleteButton = Delete; + deleteButton = Delete; } return (
@@ -277,7 +277,7 @@ module.exports = React.createClass({ blurToCancel={ false } onValueChanged={ self.onAliasAdded } />
- Add + Add
From dcfcc51f4c705a918e0b8e0545056feee5f01cbf Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 14 Jan 2016 17:28:53 +0000 Subject: [PATCH 45/82] Catch new invalid user name error added in https://github.com/matrix-org/synapse/pull/499 and https://github.com/matrix-org/matrix-doc/pull/263 --- src/Signup.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Signup.js b/src/Signup.js index 42468959fe..2d823b62ae 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -152,6 +152,8 @@ class Register extends Signup { } else { if (error.errcode === 'M_USER_IN_USE') { throw new Error("Username in use"); + } else if (error.errcode == 'M_INVALID_USER_NAME') { + throw new Error("User names may only contain alphanumeric characters, underscores or dots!"); } else if (error.httpStatus == 401) { throw new Error("Authorisation failed!"); } else if (error.httpStatus >= 400 && error.httpStatus < 500) { From 66bc30c0bc28de4c038a933e014ec5002adf9d5d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 14 Jan 2016 17:33:52 +0000 Subject: [PATCH 46/82] Add /me to the list --- src/SlashCommands.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index ca3a010791..4eb2adad5d 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -354,8 +354,12 @@ module.exports = { }, getCommandList: function() { - return Object.keys(commands).sort().map(function(cmdKey) { + // Return all the commands plus /me which isn't handled like normal commands + var cmds = Object.keys(commands).sort().map(function(cmdKey) { return commands[cmdKey]; - }); + }) + cmds.push(new Command("me", "", function(){})); + + return cmds; } }; From 51ce76aeab75d760c1a26badf901ab818dd741a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 10:08:16 +0000 Subject: [PATCH 47/82] M_INVALID_USERNAME to be consistent with param name --- src/Signup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Signup.js b/src/Signup.js index 2d823b62ae..fbc2a09634 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -152,7 +152,7 @@ class Register extends Signup { } else { if (error.errcode === 'M_USER_IN_USE') { throw new Error("Username in use"); - } else if (error.errcode == 'M_INVALID_USER_NAME') { + } else if (error.errcode == 'M_INVALID_USERNAME') { throw new Error("User names may only contain alphanumeric characters, underscores or dots!"); } else if (error.httpStatus == 401) { throw new Error("Authorisation failed!"); From cfb81a4aecdc6ae20866a7cd0e7ff1a5e77626ad Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 15 Jan 2016 12:02:28 +0000 Subject: [PATCH 48/82] Factor out avatar stuff to BaseAvatar. Make MemberAvatar use it instead. --- src/component-index.js | 1 + src/components/views/avatars/BaseAvatar.js | 131 +++++++++++++++++++ src/components/views/avatars/MemberAvatar.js | 86 +++--------- 3 files changed, 148 insertions(+), 70 deletions(-) create mode 100644 src/components/views/avatars/BaseAvatar.js diff --git a/src/component-index.js b/src/component-index.js index 7ae15ba12c..5fcf8e1ce0 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -32,6 +32,7 @@ module.exports.components['structures.RoomView'] = require('./components/structu module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel'); module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); +module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar'); module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton'); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js new file mode 100644 index 0000000000..0472b1c651 --- /dev/null +++ b/src/components/views/avatars/BaseAvatar.js @@ -0,0 +1,131 @@ +/* +Copyright 2015, 2016 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 AvatarLogic = require("../../../Avatar"); + +module.exports = React.createClass({ + displayName: 'BaseAvatar', + + propTypes: { + name: React.PropTypes.string.isRequired, + idName: React.PropTypes.string, // ID for generating hash colours + title: React.PropTypes.string, + url: React.PropTypes.string, // highest priority of them all + urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority] + width: React.PropTypes.number, + height: React.PropTypes.number, + resizeMethod: React.PropTypes.string, + defaultToInitialLetter: React.PropTypes.bool + }, + + getDefaultProps: function() { + return { + width: 40, + height: 40, + resizeMethod: 'crop', + defaultToInitialLetter: true + } + }, + + getInitialState: function() { + var defaultImageUrl = null; + if (this.props.defaultToInitialLetter) { + defaultImageUrl = AvatarLogic.defaultAvatarUrlForString( + this.props.idName || this.props.name + ); + } + return { + imageUrl: this.props.url || (this.props.urls ? this.props.urls[0] : null), + defaultImageUrl: defaultImageUrl, + urlsIndex: 0 + }; + }, + + componentWillReceiveProps: function(nextProps) { + // retry all the urls again, they may have changed. + if (this.props.urls && this.state.urlsIndex > 0) { + this.setState({ + urlsIndex: 0, + imageUrl: this.props.urls[0] + }); + } + }, + + onError: function(ev) { + var failedUrl = ev.target.src; + + if (this.props.urls) { + var nextIndex = this.state.urlsIndex + 1; + if (nextIndex < this.props.urls.length) { + // try another + this.setState({ + urlsIndex: nextIndex, + imageUrl: this.props.urls[nextIndex] + }); + return; + } + } + + // either no urls array or we've reached the end of it, we may have a default + // we can use... + if (this.props.defaultToInitialLetter) { + if (failedUrl === this.state.defaultImageUrl) { + return; // don't tightloop if the browser can't load the default URL + } + this.setState({ imageUrl: this.state.defaultImageUrl }) + } + }, + + _getInitialLetter: function() { + var name = this.props.name; + var initial = name[0]; + if (initial === '@' && name[1]) { + initial = name[1]; + } + return initial.toUpperCase(); + }, + + render: function() { + var name = this.props.name; + + if (this.state.imageUrl === this.state.defaultImageUrl) { + var initialLetter = this._getInitialLetter(); + return ( + + + + + ); + } + return ( + + ); + } +}); diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index f209006b1c..5e2dbbb23a 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -18,22 +18,16 @@ limitations under the License. var React = require('react'); var Avatar = require('../../../Avatar'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); +var sdk = require("../../../index"); module.exports = React.createClass({ displayName: 'MemberAvatar', propTypes: { - member: React.PropTypes.object, + member: React.PropTypes.object.isRequired, width: React.PropTypes.number, height: React.PropTypes.number, - resizeMethod: React.PropTypes.string, - /** - * The custom display name to use for this member. This can serve as a - * drop in replacement for RoomMember objects, or as a clobber name on - * an existing RoomMember. Used for 3pid invites. - */ - customDisplayName: React.PropTypes.string + resizeMethod: React.PropTypes.string }, getDefaultProps: function() { @@ -45,77 +39,29 @@ module.exports = React.createClass({ }, getInitialState: function() { - var defaultImageUrl = Avatar.defaultAvatarUrlForString( - this.props.customDisplayName || this.props.member.userId - ) - return { - imageUrl: this._getMemberImageUrl() || defaultImageUrl, - defaultImageUrl: defaultImageUrl - }; + return this._getState(this.props); }, componentWillReceiveProps: function(nextProps) { - this.refreshUrl(); + this.setState(this._getState(nextProps)); }, - onError: function(ev) { - // don't tightloop if the browser can't load a data url - if (ev.target.src == this.state.defaultImageUrl) { - return; - } - this.setState({ - imageUrl: this.state.defaultImageUrl - }); - }, - - _getMemberImageUrl: function() { - if (!this.props.member) { return null; } - - return Avatar.avatarUrlForMember(this.props.member, - this.props.width, - this.props.height, - this.props.resizeMethod); - }, - - _getInitialLetter: function() { - var name = this.props.customDisplayName || this.props.member.name; - var initial = name[0]; - if (initial === '@' && name[1]) { - initial = name[1]; - } - return initial.toUpperCase(); - }, - - refreshUrl: function() { - var newUrl = this._getMemberImageUrl(); - if (newUrl != this.currentUrl) { - this.currentUrl = newUrl; - this.setState({imageUrl: newUrl}); + _getState: function(props) { + return { + name: props.member.name, + title: props.member.userId, + imageUrl: Avatar.avatarUrlForMember(props.member, + props.width, + props.height, + props.resizeMethod) } }, render: function() { - var name = this.props.customDisplayName || this.props.member.name; - - if (this.state.imageUrl === this.state.defaultImageUrl) { - var initialLetter = this._getInitialLetter(); - return ( - - - - - ); - } + var BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); return ( - + ); } }); From 8e9e33fa2a45a646fcd2ef87e7b85f2634ef65c3 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 12:34:53 +0000 Subject: [PATCH 49/82] fix NPE and make enter work again --- src/components/views/elements/EditableText.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 78d9d3d0c2..7f64496393 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -154,21 +154,24 @@ module.exports = React.createClass({ //ev.target.setSelectionRange(0, ev.target.textContent.length); var node = ev.target.childNodes[0]; - var range = document.createRange(); - range.setStart(node, 0); - range.setEnd(node, node.length); - - var sel = window.getSelection(); - sel.removeAllRanges(); - sel.addRange(range); + if (node) { + var range = document.createRange(); + range.setStart(node, 0); + range.setEnd(node, node.length); + + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } }, onFinish: function(ev) { var self = this; + var submit = (ev.key === "Enter"); this.setState({ phase: this.Phases.Display, }, function() { - self.onValueChanged(ev.key === "Enter"); + self.onValueChanged(submit); }); }, From 84b46dae4e7dcb3ca0d76c0e12415bdd2e190f88 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 12:35:18 +0000 Subject: [PATCH 50/82] make ChangeAvatar customisable size... --- src/components/views/settings/ChangeAvatar.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index f5ec6a0467..7f19b193f7 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -25,6 +25,8 @@ module.exports = React.createClass({ room: React.PropTypes.object, // if false, you need to call changeAvatar.onFileSelected yourself. showUploadSection: React.PropTypes.bool, + width: React.PropTypes.number, + height: React.PropTypes.number, className: React.PropTypes.string }, @@ -37,7 +39,9 @@ module.exports = React.createClass({ getDefaultProps: function() { return { showUploadSection: true, - className: "mx_Dialog_content" // FIXME - shouldn't be this by default + className: "mx_Dialog_content", // FIXME - shouldn't be this by default + width: 80, + height: 80, }; }, @@ -111,13 +115,14 @@ module.exports = React.createClass({ // Having just set an avatar we just display that since it will take a little // time to propagate through to the RoomAvatar. if (this.props.room && !this.avatarSet) { - avatarImg = ; + avatarImg = ; } else { var style = { - maxWidth: 240, - maxHeight: 240, + width: this.props.width, + height: this.props.height, objectFit: 'cover', }; + // FIXME: surely we should be using MemberAvatar or UserAvatar or something here... avatarImg = ; } From 828b1f48377e8ff23c723b1e18829ae09fc339a0 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 12:35:30 +0000 Subject: [PATCH 51/82] fix up look and feel of UserSettings a bit more --- src/components/structures/UserSettings.js | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ddf4229170..def215ceb3 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -83,6 +83,12 @@ module.exports = React.createClass({ } }, + onAvatarPickerClick: function(ev) { + if (this.refs.file_label) { + this.refs.file_label.click(); + } + }, + onAvatarSelected: function(ev) { var self = this; var changeAvatar = this.refs.changeAvatar; @@ -175,7 +181,7 @@ module.exports = React.createClass({ if (MatrixClientPeg.get().isGuest()) { accountJsx = (
- Upgrade (It's free!) + Create an account
); } @@ -224,14 +230,14 @@ module.exports = React.createClass({ })}
-
+
-
@@ -241,13 +247,12 @@ module.exports = React.createClass({

Account

- {accountJsx} -
- -
-
+ +
Log out
+ + {accountJsx}

Notifications

From e1e46be220c285f3cb139982fef672d5a3d74b40 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 13:11:14 +0000 Subject: [PATCH 52/82] apply gemini scrollbar --- src/components/structures/UserSettings.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index def215ceb3..07c5a680c6 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -21,6 +21,7 @@ var dis = require("../../dispatcher"); var q = require('q'); var version = require('../../../package.json').version; var UserSettingsStore = require('../../UserSettingsStore'); +var GeminiScrollbar = require('react-gemini-scrollbar'); module.exports = React.createClass({ displayName: 'UserSettings', @@ -202,6 +203,8 @@ module.exports = React.createClass({
+ +

Profile

@@ -286,6 +289,8 @@ module.exports = React.createClass({ Version {this.state.clientVersion}
+ +
); } From f2dc1e835b817f3c09349c669f0be4839ff7b413 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 13:11:37 +0000 Subject: [PATCH 53/82] placeholder for displayname --- src/components/views/settings/ChangeDisplayName.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js index 8b31fbf1e3..ed5eb3fa42 100644 --- a/src/components/views/settings/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.js @@ -99,7 +99,9 @@ module.exports = React.createClass({ var EditableText = sdk.getComponent('elements.EditableText'); return ( ); } From 7cc5925ec4ad4fb0f85000d1d4c0731bbbea4c75 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 13:28:41 +0000 Subject: [PATCH 54/82] increase initialSyncLimit enormously to avoid slow gappy /syncs for now --- src/components/structures/MatrixChat.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index e5af2a86b5..df74cf5de1 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -522,7 +522,9 @@ module.exports = React.createClass({ UserActivity.start(); Presence.start(); cli.startClient({ - pendingEventOrdering: "end" + pendingEventOrdering: "end", + // deliberately huge limit for now to avoid hitting gappy /sync's until gappy /sync performance improves + initialSyncLimit: 250, }); }, From 02e41450b4cf220a9d0ed4be9a7b6da41b462362 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 13:31:41 +0000 Subject: [PATCH 55/82] Do (more) client side validation of registration parameters. --- package.json | 3 +- .../structures/login/Registration.js | 9 ++ .../views/login/RegistrationForm.js | 133 +++++++++++++++--- 3 files changed, 124 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 9c2c645ea2..ac72744af4 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "react-dom": "^0.14.2", "react-gemini-scrollbar": "^2.0.1", "sanitize-html": "^1.11.1", - "velocity-animate": "^1.2.3" + "velocity-animate": "^1.2.3", + "velocity-ui-pack": "^1.2.2" }, "//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder", "//depsbuglink": "https://github.com/webpack/webpack/issues/1472", diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index f89d65d740..7b2808c72a 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -159,6 +159,15 @@ module.exports = React.createClass({ case "RegistrationForm.ERR_PASSWORD_LENGTH": errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`; break; + case "RegistrationForm.ERR_EMAIL_INVALID": + errMsg = "This doesn't look like a valid email address"; + break; + case "RegistrationForm.ERR_USERNAME_INVALID": + errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores."; + break; + case "RegistrationForm.ERR_USERNAME_BLANK": + errMsg = "You need to enter a user name"; + break; default: console.error("Unknown error code: %s", errCode); errMsg = "An unknown error occurred."; diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 534464a4ae..d59f6556d7 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -17,8 +17,15 @@ limitations under the License. 'use strict'; var React = require('react'); +var Velocity = require('velocity-animate'); +require('velocity-ui-pack'); var sdk = require('../../../index'); +var FIELD_EMAIL = 'field_email'; +var FIELD_USERNAME = 'field_username'; +var FIELD_PASSWORD = 'field_password'; +var FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; + /** * A pure UI component which displays a registration form. */ @@ -50,31 +57,14 @@ module.exports = React.createClass({ email: this.props.defaultEmail, username: this.props.defaultUsername, password: null, - passwordConfirm: null + passwordConfirm: null, + fieldValid: {} }; }, onSubmit: function(ev) { ev.preventDefault(); - var pwd1 = this.refs.password.value.trim(); - var pwd2 = this.refs.passwordConfirm.value.trim() - - var errCode; - if (!pwd1 || !pwd2) { - errCode = "RegistrationForm.ERR_PASSWORD_MISSING"; - } - else if (pwd1 !== pwd2) { - errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH"; - } - else if (pwd1.length < this.props.minPasswordLength) { - errCode = "RegistrationForm.ERR_PASSWORD_LENGTH"; - } - if (errCode) { - this.props.onError(errCode); - return; - } - var promise = this.props.onRegisterClick({ username: this.refs.username.value.trim(), password: pwd1, @@ -89,13 +79,110 @@ module.exports = React.createClass({ } }, + validateField: function(field_id) { + var pwd1 = this.refs.password.value.trim(); + var pwd2 = this.refs.passwordConfirm.value.trim() + + switch (field_id) { + case FIELD_EMAIL: + this.markFieldValid( + field_id, + this.refs.email.value == '' || !!this.refs.email.value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i), + "RegistrationForm.ERR_EMAIL_INVALID" + ); + break; + case FIELD_USERNAME: + // XXX: SPEC-1 + if (encodeURIComponent(this.refs.username.value) != this.refs.username.value) { + this.markFieldValid( + field_id, + false, + "RegistrationForm.ERR_USERNAME_INVALID" + ); + } else if (this.refs.username.value == '') { + this.markFieldValid( + field_id, + false, + "RegistrationForm.ERR_USERNAME_BLANK" + ); + } else { + this.markFieldValid(field_id, true); + } + break; + case FIELD_PASSWORD: + if (pwd1 == '') { + this.markFieldValid( + field_id, + false, + "RegistrationForm.ERR_PASSWORD_MISSING" + ); + } else if (pwd1.length < this.props.minPasswordLength) { + this.markFieldValid( + field_id, + false, + "RegistrationForm.ERR_PASSWORD_MISSING" + ); + } + break; + case FIELD_PASSWORD_CONFIRM: + if (pwd1 == '') { + this.markFieldValid( + field_id, false, + "RegistrationForm.ERR_PASSWORD_MISSING" + ); + } else if (pwd1 != pwd2) { + this.markFieldValid( + field_id, false, + "RegistrationForm.ERR_PASSWORD_LENGTH" + ); + } else { + this.markFieldValid(field_id, true); + } + break; + } + }, + + markFieldValid: function(field_id, val, error_code) { + var fieldValid = this.state.fieldValid; + fieldValid[field_id] = val; + this.setState({fieldValid: fieldValid}); + if (!val) { + Velocity(this.fieldElementById(field_id), "callout.shake", 300); + this.props.onError(error_code); + } + }, + + fieldElementById(field_id) { + switch (field_id) { + case FIELD_EMAIL: + return this.refs.email; + case FIELD_USERNAME: + return this.refs.username; + case FIELD_PASSWORD: + return this.refs.password; + case FIELD_PASSWORD_CONFIRM: + return this.refs.passwordConfirm; + } + }, + + _styleField: function(field_id, baseStyle) { + var style = baseStyle || {}; + if (this.state.fieldValid[field_id] === false) { + style['borderColor'] = 'red'; + } + return style; + }, + render: function() { + var self = this; var emailSection, registerButton; if (this.props.showEmail) { emailSection = ( + defaultValue={this.state.email} + style={this._styleField(FIELD_EMAIL)} + onBlur={function() {self.validateField(FIELD_EMAIL)}} /> ); } if (this.props.onRegisterClick) { @@ -111,13 +198,19 @@ module.exports = React.createClass({



{registerButton} From 4a2c2d9656bc5c08512e7e452f2cbca86e3738e4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 14:22:17 +0000 Subject: [PATCH 56/82] fix broken merge and revert some of 243b2e45 and document evil magic numbers --- src/components/structures/RoomView.js | 16 +++---- src/components/views/rooms/RoomHeader.js | 52 ++++++++++++++++++++-- src/components/views/rooms/RoomSettings.js | 8 ++++ 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 1f409da81e..4f97e1f82f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1189,7 +1189,11 @@ module.exports = React.createClass({ // a maxHeight on the underlying remote video tag. // header + footer + status + give us at least 100px of scrollback at all times. - var auxPanelMaxHeight = window.innerHeight - (83 + 72 + 36 + 100); + var auxPanelMaxHeight = window.innerHeight - + (83 + // height of RoomHeader + 36 + // height of the status area + 72 + // minimum height of the message compmoser + 100); // amount of desired scrollback // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway // but it's better than the video going missing entirely @@ -1198,16 +1202,6 @@ module.exports = React.createClass({ if (this.refs.callView) { var video = this.refs.callView.getVideoView().getRemoteVideoElement(); - // header + footer + status + give us at least 100px of scrollback at all times. - auxPanelMaxHeight = window.innerHeight - - (83 + 72 + - sdk.getComponent('rooms.MessageComposer').MAX_HEIGHT + - 100); - - // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway - // but it's better than the video going missing entirely - if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; - video.style.maxHeight = auxPanelMaxHeight + "px"; // the above might have made the video panel resize itself, so now diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 0e69e24ede..41b1b364e2 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -90,6 +90,32 @@ module.exports = React.createClass({ this.setState({ topic : value }); }, + onAvatarPickerClick: function(ev) { + if (this.refs.file_label) { + this.refs.file_label.click(); + } + }, + + onAvatarSelected: function(ev) { + var self = this; + var changeAvatar = this.refs.changeAvatar; + if (!changeAvatar) { + console.error("No ChangeAvatar found to upload image to!"); + return; + } + changeAvatar.onFileSelected(ev).done(function() { + // dunno if the avatar changed, re-check it. + self._refreshFromServer(); + }, function(err) { + var errMsg = (typeof err === "string") ? err : (err.error || ""); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Error", + description: "Failed to set avatar. " + errMsg + }); + }); + }, + getRoomName: function() { return this.state.name; }, @@ -100,7 +126,8 @@ module.exports = React.createClass({ render: function() { var EditableText = sdk.getComponent("elements.EditableText"); - var RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); + var RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); + var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar"); var TintableSvg = sdk.getComponent("elements.TintableSvg"); var header; @@ -184,9 +211,26 @@ module.exports = React.createClass({ var roomAvatar = null; if (this.props.room) { - roomAvatar = ( - - ); + if (this.props.editing) { + roomAvatar = ( +
+ +
+ + +
+
+ ); + } + else { + roomAvatar = ( + + ); + } } var leave_button; diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 0bb27658ff..359842ce00 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -366,6 +366,12 @@ module.exports = React.createClass({
; } + var create_event = this.props.room.currentState.getStateEvents('m.room.create', ''); + var unfederatable_section; + if (create_event.getContent()["m.federate"] === false) { + unfederatable_section =
Ths room is not accessible by remote Matrix servers
. + } + // TODO: support editing custom events_levels // TODO: support editing custom user_levels @@ -383,6 +389,8 @@ module.exports = React.createClass({
+ { unfederatable_section } + { room_colors_section } { aliases_section } From 2638ee974ea70268599fb06720291908efeb09ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 15 Jan 2016 15:18:55 +0000 Subject: [PATCH 57/82] Add warning on inviting a user if sharing history with new users. Fixes https://github.com/vector-im/vector-web/issues/60 --- src/components/views/rooms/MemberList.js | 53 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index eac5466e88..f4073035af 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -16,12 +16,18 @@ limitations under the License. var React = require('react'); var classNames = require('classnames'); var Matrix = require("matrix-js-sdk"); +var q = require('q'); var MatrixClientPeg = require("../../../MatrixClientPeg"); var Modal = require("../../../Modal"); var sdk = require('../../../index'); var GeminiScrollbar = require('react-gemini-scrollbar'); var INITIAL_LOAD_NUM_MEMBERS = 50; +var SHARE_HISTORY_WARNING = "Newly invited users will see the history of this room. "+ + "If you'd prefer invited users not to see messages that were sent before they joined, "+ + "turn off, 'Share message history with new users' in the settings for this room."; + +var shown_invite_warning_this_session = false; module.exports = React.createClass({ displayName: 'MemberList', @@ -132,12 +138,41 @@ module.exports = React.createClass({ return; } - var promise; + var invite_defer = q.defer(); + + var room = MatrixClientPeg.get().getRoom(this.props.roomId); + var history_visibility = room.currentState.getStateEvents('m.room.history_visibility', ''); + if (history_visibility) history_visibility = history_visibility.getContent().history_visibility; + + if (history_visibility == 'shared' && !shown_invite_warning_this_session) { + var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createDialog(QuestionDialog, { + title: "Warning", + description: SHARE_HISTORY_WARNING, + button: "Invite", + onFinished: function(should_invite) { + if (should_invite) { + shown_invite_warning_this_session = true; + invite_defer.resolve(); + } else { + invite_defer.reject(null); + } + } + }); + } else { + invite_defer.resolve(); + } + + var promise = invite_defer.promise;; if (isEmailAddress) { - promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText); + promise = promise.then(function() { + MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText); + }); } else { - promise = MatrixClientPeg.get().invite(this.props.roomId, inputText); + promise = promise.then(function() { + MatrixClientPeg.get().invite(self.props.roomId, inputText); + }); } self.setState({ @@ -152,11 +187,13 @@ module.exports = React.createClass({ inviting: false }); }, function(err) { - console.error("Failed to invite: %s", JSON.stringify(err)); - Modal.createDialog(ErrorDialog, { - title: "Server error whilst inviting", - description: err.message - }); + if (err !== null) { + console.error("Failed to invite: %s", JSON.stringify(err)); + Modal.createDialog(ErrorDialog, { + title: "Server error whilst inviting", + description: err.message + }); + } self.setState({ inviting: false }); From b8afccd445b352d26c9e55be9b9f7c0296d8d1b7 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 15:23:02 +0000 Subject: [PATCH 58/82] fix droptarget behaviour --- src/components/structures/RoomView.js | 35 +++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4f97e1f82f..36773672b1 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -142,16 +142,16 @@ module.exports = React.createClass({ // (We could use isMounted, but facebook have deprecated that.) this.unmounted = true; - if (this.refs.messagePanel) { - // disconnect the D&D event listeners from the message panel. This - // is really just for hygiene - the messagePanel is going to be + if (this.refs.roomView) { + // disconnect the D&D event listeners from the room view. This + // is really just for hygiene - we're going to be // deleted anyway, so it doesn't matter if the event listeners // don't get cleaned up. - var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); - messagePanel.removeEventListener('drop', this.onDrop); - messagePanel.removeEventListener('dragover', this.onDragOver); - messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd); - messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd); + var roomView = ReactDOM.findDOMNode(this.refs.roomView); + roomView.removeEventListener('drop', this.onDrop); + roomView.removeEventListener('dragover', this.onDragOver); + roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); + roomView.removeEventListener('dragend', this.onDragLeaveOrEnd); } dis.unregister(this.dispatcherRef); if (MatrixClientPeg.get()) { @@ -414,6 +414,14 @@ module.exports = React.createClass({ window.addEventListener('resize', this.onResize); this.onResize(); + if (this.refs.roomView) { + var roomView = ReactDOM.findDOMNode(this.refs.roomView); + roomView.addEventListener('drop', this.onDrop); + roomView.addEventListener('dragover', this.onDragOver); + roomView.addEventListener('dragleave', this.onDragLeaveOrEnd); + roomView.addEventListener('dragend', this.onDragLeaveOrEnd); + } + this._updateTabCompleteList(this.state.room); }, @@ -432,11 +440,6 @@ module.exports = React.createClass({ var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel); this.refs.messagePanel.initialised = true; - messagePanel.addEventListener('drop', this.onDrop); - messagePanel.addEventListener('dragover', this.onDragOver); - messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd); - messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd); - this.scrollToBottom(); this.sendReadReceipt(); @@ -1439,7 +1442,7 @@ module.exports = React.createClass({ fileDropTarget =

- Drop File Here + Drop file here to upload
; } @@ -1540,7 +1543,7 @@ module.exports = React.createClass({ ); return ( -
+
- { fileDropTarget }
+ { fileDropTarget } { conferenceCallNotification } From aefecfc645aff7eab5c57415bdd1f17d3534c25c Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 15:23:41 +0000 Subject: [PATCH 59/82] tweak roomheader layout when editing --- src/components/views/rooms/RoomHeader.js | 8 +++++--- src/components/views/settings/ChangeAvatar.js | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 41b1b364e2..79eac7b252 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -213,7 +213,7 @@ module.exports = React.createClass({ if (this.props.room) { if (this.props.editing) { roomAvatar = ( -
+
; - var change_avatar; - if (can_set_room_avatar) { - change_avatar = -
-

Room Icon

- -
; - } - var user_levels_section; if (user_levels.length) { user_levels_section = @@ -369,7 +359,7 @@ module.exports = React.createClass({ var create_event = this.props.room.currentState.getStateEvents('m.room.create', ''); var unfederatable_section; if (create_event.getContent()["m.federate"] === false) { - unfederatable_section =
Ths room is not accessible by remote Matrix servers
. + unfederatable_section =
Ths room is not accessible by remote Matrix servers.
} // TODO: support editing custom events_levels @@ -447,8 +437,6 @@ module.exports = React.createClass({ { banned_users_section } - { change_avatar } -
); } From 0caf5e0cdc18874ec26a21ed891bc4654dfb10fd Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 15 Jan 2016 15:51:40 +0000 Subject: [PATCH 61/82] manage permissions on whether people can actually edit name, topic, avatar etc or not --- src/components/views/rooms/RoomHeader.js | 78 +++++++++++++++++------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 79eac7b252..606603d451 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -151,20 +151,53 @@ module.exports = React.createClass({ var cancel_button = null; var save_button = null; var settings_button = null; - // var actual_name = this.props.room.currentState.getStateEvents('m.room.name', ''); - // if (actual_name) actual_name = actual_name.getContent().name; if (this.props.editing) { - // name = - //
- // - //
- // if (topic) topic_el =
+ + // calculate permissions. XXX: this should be done on mount or something, and factored out with RoomSettings + var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); + var events_levels = power_levels.events || {}; + var user_id = MatrixClientPeg.get().credentials.userId; + + if (power_levels) { + power_levels = power_levels.getContent(); + var default_user_level = parseInt(power_levels.users_default || 0); + var user_levels = power_levels.users || {}; + var current_user_level = user_levels[user_id]; + if (current_user_level == undefined) current_user_level = default_user_level; + } else { + var default_user_level = 0; + var user_levels = []; + var current_user_level = 0; + } + + var room_avatar_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.avatar'] !== undefined) { + room_avatar_level = events_levels['m.room.avatar']; + } + var can_set_room_avatar = current_user_level >= room_avatar_level; + + var room_name_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.name'] !== undefined) { + room_name_level = events_levels['m.room.name']; + } + var can_set_room_name = current_user_level >= room_name_level; + + var room_topic_level = parseInt(power_levels.state_default || 0); + if (events_levels['m.room.topic'] !== undefined) { + room_topic_level = events_levels['m.room.topic']; + } + var can_set_room_topic = current_user_level >= room_topic_level; var placeholderName = "Unnamed Room"; if (this.state.defaultName && this.state.defaultName !== '?') { placeholderName += " (" + this.state.defaultName + ")"; } + save_button =
Save
+ cancel_button =
Cancel
+ } + + if (can_set_room_name) { name =
- - topic_el = - - - save_button =
Save
- cancel_button =
Cancel
- } else { - // + } + else { var searchStatus; // don't display the search count until the search completes and // gives us a valid (possibly zero) searchCount. @@ -204,17 +225,28 @@ module.exports = React.createClass({
+ } + if (can_set_room_topic) { + topic_el = + + } else { var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); if (topic) topic_el =
{ topic.getContent().topic }
; } var roomAvatar = null; if (this.props.room) { - if (this.props.editing) { + if (can_set_room_avatar) { roomAvatar = (
- +