diff --git a/CHANGELOG.md b/CHANGELOG.md index 262d55c6da..70f946d7cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +Changes in [0.6.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.3) (2016-06-03) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.2...v0.6.3) + + * Change invite text field wording + * Fix bug with new email invite UX where the invite could get wedged + * Label app versions sensibly in UserSettings + +Changes in [0.6.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.2) (2016-06-02) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.1...v0.6.2) + + * Correctly bump dep on matrix-js-sdk 0.5.4 + +Changes in [0.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.1) (2016-06-02) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.0...v0.6.1) + + * Fix focusing race in new UX for 3pid invites + * Fix jenkins.sh + +Changes in [0.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.0) (2016-06-02) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.2...v0.6.0) + + * implement new UX for 3pid invites + [\#297](https://github.com/matrix-org/matrix-react-sdk/pull/297) + * multiple URL preview support + [\#290](https://github.com/matrix-org/matrix-react-sdk/pull/290) + * Add a fallback home server to log into + [\#293](https://github.com/matrix-org/matrix-react-sdk/pull/293) + * Hopefully fix memory leak with velocity + [\#291](https://github.com/matrix-org/matrix-react-sdk/pull/291) + * Support for enabling email notifications + [\#289](https://github.com/matrix-org/matrix-react-sdk/pull/289) + * Correct Readme instructions how to customize the UI + [\#286](https://github.com/matrix-org/matrix-react-sdk/pull/286) + * Avoid rerendering during Room unmount + [\#285](https://github.com/matrix-org/matrix-react-sdk/pull/285) + Changes in [0.5.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.5.2) (2016-04-22) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.5.1...v0.5.2) diff --git a/jenkins.sh b/jenkins.sh index 51fab5d020..eeb7d7d56e 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -8,9 +8,6 @@ nvm use 4 set -x -# install the version of js-sdk provided to us by jenkins -npm install ./node_modules/matrix-js-sdk-*.tgz - # install the other dependencies npm install diff --git a/package.json b/package.json index 156de085eb..3ae985d49d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.5.2", + "version": "0.6.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -31,7 +31,7 @@ "highlight.js": "^8.9.1", "linkifyjs": "^2.0.0-beta.4", "marked": "^0.3.5", - "matrix-js-sdk": "^0.5.2", + "matrix-js-sdk": "^0.5.4", "optimist": "^0.6.1", "q": "^1.4.1", "react": "^15.0.1", diff --git a/src/Signup.js b/src/Signup.js index 5b368b4811..4518955d95 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -293,8 +293,9 @@ class Register extends Signup { class Login extends Signup { - constructor(hsUrl, isUrl) { + constructor(hsUrl, isUrl, fallbackHsUrl) { super(hsUrl, isUrl); + this._fallbackHsUrl = fallbackHsUrl; this._currentFlowIndex = 0; this._flows = []; } @@ -359,6 +360,30 @@ class Login extends Signup { error.friendlyText = ( 'Incorrect username and/or password.' ); + if (self._fallbackHsUrl) { + // as per elsewhere, it would be much nicer to not replace the global + // client just to try an alternate HS + MatrixClientPeg.replaceUsingUrls( + self._fallbackHsUrl, + self._isUrl + ); + return MatrixClientPeg.get().login('m.login.password', loginParams).then(function(data) { + return q({ + homeserverUrl: self._fallbackHsUrl, + identityServerUrl: self._isUrl, + userId: data.user_id, + accessToken: data.access_token + }); + }, function(fallback_error) { + // We also have to put the default back again if it fails... + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + // throw the original error + throw error; + }); + } } else { error.friendlyText = ( diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 5a43d41dd5..e4c0d5973a 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -330,7 +330,7 @@ module.exports = { * Returns null if the input didn't match a command. */ processInput: function(roomId, input) { - // trim any trailing whitespace, as it can confuse the parser for + // trim any trailing whitespace, as it can confuse the parser for // IRC-style commands input = input.replace(/\s+$/, ""); if (input[0] === "/" && input[1] !== "/") { diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index cf7131eb7b..9bb1388e76 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -100,7 +100,7 @@ module.exports = { return this.getEmailPusher(pushers, address) !== undefined; }, - addEmailPusher: function(address) { + addEmailPusher: function(address, data) { return MatrixClientPeg.get().setPusher({ kind: 'email', app_id: "m.email", @@ -108,7 +108,7 @@ module.exports = { app_display_name: 'Email Notifications', device_display_name: address, lang: navigator.language, - data: {}, + data: data, append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address }); }, diff --git a/src/Velociraptor.js b/src/Velociraptor.js index ad12d1323b..0abf34b230 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -106,6 +106,18 @@ module.exports = React.createClass({ }); //console.log("enter: "+JSON.stringify(node.props._restingStyle)); + } else if (node === null) { + // Velocity stores data on elements using the jQuery .data() + // method, and assumes you'll be using jQuery's .remove() to + // remove the element, but we don't use jQuery, so we need to + // blow away the element's data explicitly otherwise it will leak. + // This uses Velocity's internal jQuery compatible wrapper. + // See the bug at + // https://github.com/julianshapiro/velocity/issues/300 + // and the FAQ entry, "Preventing memory leaks when + // creating/destroying large numbers of elements" + // (https://github.com/julianshapiro/velocity/issues/47) + Velocity.Utilities.removeData(this.nodes[k]); } this.nodes[k] = node; }, diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0506dad78b..9aad4e72de 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -104,6 +104,10 @@ module.exports = React.createClass({ return "https://matrix.org"; }, + getFallbackHsUrl: function() { + return this.props.config.fallback_hs_url; + }, + getCurrentIsUrl: function() { if (this.state.register_is_url) { return this.state.register_is_url; @@ -490,6 +494,7 @@ module.exports = React.createClass({ }, type: 'm.room.guest_access', state_key: '', + visibility: 'private', } ], }).done(function(res) { @@ -1157,6 +1162,7 @@ module.exports = React.createClass({ guestAccessToken={this.state.guestAccessToken} defaultHsUrl={this.props.config.default_hs_url} defaultIsUrl={this.props.config.default_is_url} + brand={this.props.config.brand} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} registrationUrl={this.props.registrationUrl} @@ -1185,6 +1191,7 @@ module.exports = React.createClass({ defaultIsUrl={this.props.config.default_is_url} customHsUrl={this.getCurrentHsUrl()} customIsUrl={this.getCurrentIsUrl()} + fallbackHsUrl={this.getFallbackHsUrl()} onForgotPasswordClick={this.onForgotPasswordClick} onLoginAsGuestClick={this.props.enableGuest && this.props.config && this.props.config.default_hs_url ? this._registerAsGuest.bind(this, true) : undefined} onCancelClick={ this.state.guestCreds ? this.onReturnToGuestClick : null } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 33bbb510e3..77080b5a75 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -677,6 +677,16 @@ module.exports = React.createClass({ uploadFile: function(file) { var self = this; + + if (MatrixClientPeg.get().isGuest()) { + var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); + Modal.createDialog(NeedToRegisterDialog, { + title: "Please Register", + description: "Guest users can't upload files. Please register to upload." + }); + return; + } + ContentMessages.sendContentToRoom( file, this.state.room.roomId, MatrixClientPeg.get() ).done(undefined, function(error) { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index e15993a07c..d804dfd6b9 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -401,7 +401,7 @@ var TimelinePanel = React.createClass({ // if we are scrolled to the bottom, do a quick-reset of our unreadNotificationCount // to avoid having to wait from the remote echo from the homeserver. - if (this.getScrollState().stuckAtBottom) { + if (this.isAtEndOfLiveTimeline()) { this.props.room.setUnreadNotificationCount('total', 0); this.props.room.setUnreadNotificationCount('highlight', 0); // XXX: i'm a bit surprised we don't have to emit an event or dispatch to get this picked up diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index e2a28f0cef..e56e5d9d87 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -299,7 +299,7 @@ module.exports = React.createClass({ onValueChanged={ this.onAddThreepidClicked } />
- Add + Add
); @@ -397,9 +397,14 @@ module.exports = React.createClass({ Logged in as {this._me}
- Version {this.state.clientVersion} -
- {this.props.version} + Homeserver is { MatrixClientPeg.get().getHomeserverUrl() } +
+
+ Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() } +
+
+ matrix-react-sdk version: {this.state.clientVersion}
+ vector-web version: {this.props.version}
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index d127c7ed78..aa0c42dc98 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -35,6 +35,10 @@ module.exports = React.createClass({displayName: 'Login', customIsUrl: React.PropTypes.string, defaultHsUrl: React.PropTypes.string, defaultIsUrl: React.PropTypes.string, + // Secondary HS which we try to log into if the user is using + // the default HS but login fails. Useful for migrating to a + // different home server without confusing users. + fallbackHsUrl: React.PropTypes.string, // login shouldn't know or care how registration is done. onRegisterClick: React.PropTypes.func.isRequired, @@ -105,7 +109,9 @@ module.exports = React.createClass({displayName: 'Login', hsUrl = hsUrl || this.state.enteredHomeserverUrl; isUrl = isUrl || this.state.enteredIdentityServerUrl; - var loginLogic = new Signup.Login(hsUrl, isUrl); + var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; + + var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl); this._loginLogic = loginLogic; loginLogic.getFlows().then(function(flows) { diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index d852991b9c..2f15a3b5df 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -22,6 +22,7 @@ var sdk = require('../../../index'); var dis = require('../../../dispatcher'); var Signup = require("../../../Signup"); var ServerConfig = require("../../views/login/ServerConfig"); +var MatrixClientPeg = require("../../../MatrixClientPeg"); var RegistrationForm = require("../../views/login/RegistrationForm"); var CaptchaForm = require("../../views/login/CaptchaForm"); @@ -40,6 +41,7 @@ module.exports = React.createClass({ customIsUrl: React.PropTypes.string, defaultHsUrl: React.PropTypes.string, defaultIsUrl: React.PropTypes.string, + brand: React.PropTypes.string, email: React.PropTypes.string, username: React.PropTypes.string, guestAccessToken: React.PropTypes.string, @@ -145,6 +147,26 @@ module.exports = React.createClass({ identityServerUrl: self.registerLogic.getIdentityServerUrl(), accessToken: response.access_token }); + + if (self.props.brand) { + MatrixClientPeg.get().getPushers().done((resp)=>{ + var pushers = resp.pushers; + for (var i = 0; i < pushers.length; ++i) { + if (pushers[i].kind == 'email') { + var emailPusher = pushers[i]; + emailPusher.data = { brand: self.props.brand }; + MatrixClientPeg.get().setPusher(emailPusher).done(() => { + console.log("Set email branding to " + self.props.brand); + }, (error) => { + console.error("Couldn't set email branding: " + error); + }); + } + } + }, (error) => { + console.error("Couldn't get pushers: " + error); + }); + } + }, function(err) { if (err.message) { self.setState({ diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index d81ae98718..fed7ff079a 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -39,11 +39,11 @@ module.exports = React.createClass({ focus: true }; }, - + componentDidMount: function() { if (this.props.focus) { - // Set the cursor at the end of the text input - this.refs.textinput.value = this.props.value; + // Set the cursor at the end of the text input + this.refs.textinput.value = this.props.value; } }, @@ -83,13 +83,12 @@ module.exports = React.createClass({
- - +
); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 223eabdc36..310da598fa 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -45,9 +45,9 @@ module.exports = React.createClass({ getInitialState: function() { return { - // the URL (if any) to be previewed with a LinkPreviewWidget + // the URLs (if any) to be previewed with a LinkPreviewWidget // inside this TextualBody. - link: null, + links: [], // track whether the preview widget is hidden widgetHidden: false, @@ -57,9 +57,11 @@ module.exports = React.createClass({ componentDidMount: function() { linkifyElement(this.refs.content, linkifyMatrix.options); - var link = this.findLink(this.refs.content.children); - if (link) { - this.setState({ link: link.getAttribute("href") }); + var links = this.findLinks(this.refs.content.children); + if (links.length) { + this.setState({ links: links.map((link)=>{ + return link.getAttribute("href"); + })}); // lazy-load the hidden state of the preview widget from localstorage if (global.localStorage) { @@ -74,27 +76,32 @@ module.exports = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { // exploit that events are immutable :) + // ...and that .links is only ever set in componentDidMount and never changes return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || nextProps.highlights !== this.props.highlights || nextProps.highlightLink !== this.props.highlightLink || - nextState.link !== this.state.link || + nextState.links !== this.state.links || nextState.widgetHidden !== this.state.widgetHidden); }, - findLink: function(nodes) { + findLinks: function(nodes) { + var links = []; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node.tagName === "A" && node.getAttribute("href")) { - return this.isLinkPreviewable(node) ? node : undefined; + if (this.isLinkPreviewable(node)) { + links.push(node); + } } else if (node.tagName === "PRE" || node.tagName === "CODE") { - return; + continue; } else if (node.children && node.children.length) { - return this.findLink(node.children) + links = links.concat(this.findLinks(node.children)); } } + return links; }, isLinkPreviewable: function(node) { @@ -117,7 +124,7 @@ module.exports = React.createClass({ else { var url = node.getAttribute("href"); var host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1]; - if (node.textContent.trim().startsWith(host)) { + if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) { // it's a "foo.pl" style link return; } @@ -160,14 +167,17 @@ module.exports = React.createClass({ {highlightLink: this.props.highlightLink}); - var widget; - if (this.state.link && !this.state.widgetHidden) { + var widgets; + if (this.state.links.length && !this.state.widgetHidden) { var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget'); - widget = ; + widgets = this.state.links.map((link)=>{ + return ; + }); } switch (content.msgtype) { @@ -176,21 +186,21 @@ module.exports = React.createClass({ return ( * { name } { body } - { widget } + { widgets } ); case "m.notice": return ( { body } - { widget } + { widgets } ); default: // including "m.text" return ( { body } - { widget } + { widgets } ); } diff --git a/src/components/views/rooms/InviteMemberList.js b/src/components/views/rooms/InviteMemberList.js index 480066771b..5246e2e54d 100644 --- a/src/components/views/rooms/InviteMemberList.js +++ b/src/components/views/rooms/InviteMemberList.js @@ -26,6 +26,7 @@ module.exports = React.createClass({ propTypes: { roomId: React.PropTypes.string.isRequired, onInvite: React.PropTypes.func.isRequired, // fn(inputText) + onThirdPartyInvite: React.PropTypes.func.isRequired, // fn(inputText) onSearchQueryChanged: React.PropTypes.func // fn(inputText) }, @@ -49,10 +50,19 @@ module.exports = React.createClass({ } }, + componentDidMount: function() { + // initialise the email tile + this.onSearchQueryChanged(''); + }, + onInvite: function(ev) { this.props.onInvite(this._input); }, + onThirdPartyInvite: function(ev) { + this.props.onThirdPartyInvite(this._input); + }, + onSearchQueryChanged: function(input) { this._input = input; var EntityTile = sdk.getComponent("rooms.EntityTile"); @@ -68,9 +78,10 @@ module.exports = React.createClass({ this._emailEntity = new Entities.newEntity( } - className="mx_EntityTile_invitePlaceholder" - presenceState="online" onClick={this.onInvite} name={label} />, + avatarJsx={ } + className="mx_EntityTile_invitePlaceholder" + presenceState="online" onClick={this.onThirdPartyInvite} name={"Invite by email"} + />, function(query) { return true; // always show this } @@ -89,7 +100,7 @@ module.exports = React.createClass({ } return ( - { + if (should_invite) { + // defer the actual invite to the next event loop to give this + // Modal a chance to unmount in case onInvite() triggers a new one + setTimeout(()=>{ + this.onInvite(addresses); + }, 0); + } + } + }); + }, + onInvite: function(inputText) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); @@ -514,6 +533,7 @@ module.exports = React.createClass({ inviteMemberListSection = ( ); } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 20785c4c70..18d138f013 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -46,6 +46,15 @@ module.exports = React.createClass({ }, onUploadClick: function(ev) { + if (MatrixClientPeg.get().isGuest()) { + var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); + Modal.createDialog(NeedToRegisterDialog, { + title: "Please Register", + description: "Guest users can't upload files. Please register to upload." + }); + return; + } + this.refs.uploadInput.click(); }, diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 7d331c7eea..dc1c89d11d 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -36,7 +36,7 @@ module.exports = React.createClass({ getInitialState: function() { var tags = {}; Object.keys(this.props.room.tags).forEach(function(tagName) { - tags[tagName] = {}; + tags[tagName] = ['yep']; }); var areNotifsMuted = false; @@ -186,7 +186,7 @@ module.exports = React.createClass({ // tags if (this.state.tags_changed) { var tagDiffs = ObjectUtils.getKeyValueArrayDiffs(originalState.tags, this.state.tags); - // [ {place: add, key: "m.favourite", val: "yep"} ] + // [ {place: add, key: "m.favourite", val: ["yep"]} ] tagDiffs.forEach(function(diff) { switch (diff.place) { case "add": diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js index c09fc2faee..a22126025c 100644 --- a/src/components/views/rooms/SearchableEntityList.js +++ b/src/components/views/rooms/SearchableEntityList.js @@ -48,6 +48,7 @@ var SearchableEntityList = React.createClass({ getInitialState: function() { return { query: "", + focused: false, truncateAt: this.props.truncateAt, results: this.getSearchResults("", this.props.entities) }; @@ -101,7 +102,7 @@ var SearchableEntityList = React.createClass({ getSearchResults: function(query, entities) { if (!query || query.length === 0) { - return this.props.emptyQueryShowsAll ? entities : [] + return this.props.emptyQueryShowsAll ? entities : [ entities[0] ] } return entities.filter(function(e) { return e.matches(query); @@ -134,13 +135,27 @@ var SearchableEntityList = React.createClass({
{ + if (this._blurTimeout) { + clearTimeout(this.blurTimeout); + } + this.setState({ focused: true }); + } } + onBlur={ ()=>{ + // nasty setTimeout heuristic to avoid the 'invite by email' prompt disappearing + // due to the onBlur before we can click on it + this._blurTimeout = setTimeout( + ()=>{ this.setState({ focused: false }) }, + 300 + ); + } } placeholder={this.props.searchPlaceholderText} />
); } var list; - if (this.state.results.length) { + if (this.state.results.length > 1 || this.state.focused) { if (this.props.truncateAt) { // caller wants list truncated var TruncatedList = sdk.getComponent("elements.TruncatedList"); list = ( @@ -172,10 +187,10 @@ var SearchableEntityList = React.createClass({ } return ( -
+
{ inputBox } { list } - { this.state.query.length ?

: '' } + { list ?

: '' }
); }