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 } />
-

+
);
@@ -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({
);
}
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 ?
: '' }
);
}