Merge remote-tracking branch 'origin/develop' into unread_sync
This commit is contained in:
commit
13e70e6956
61 changed files with 4360 additions and 890 deletions
package.json
src
AddThreepid.jsAvatar.jsEntities.jsGuestAccess.jsMatrixClientPeg.jsPasswordReset.jsPresence.jsSignup.jsSlashCommands.jsTabComplete.jsTabCompleteEntries.jsTextForEvent.jsTinter.jsUserActivity.jsUserSettingsStore.jsVelociraptor.jscomponent-index.jsemail.js
components
structures
views
avatars
dialogs
elements
login
messages
rooms
EntityTile.jsMemberInfo.jsMemberList.jsMemberTile.jsMessageComposer.jsPresenceLabel.jsRoomHeader.jsRoomPreviewBar.jsRoomSettings.jsRoomTile.jsSearchableEntityList.jsTabCompleteBar.jsUserTile.js
settings
voip
|
@ -35,7 +35,8 @@
|
||||||
"react-dom": "^0.14.2",
|
"react-dom": "^0.14.2",
|
||||||
"react-gemini-scrollbar": "^2.0.1",
|
"react-gemini-scrollbar": "^2.0.1",
|
||||||
"sanitize-html": "^1.11.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",
|
"//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",
|
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",
|
||||||
|
|
76
src/AddThreepid.js
Normal file
76
src/AddThreepid.js
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
Copyright 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 MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows a user to add a third party identifier to their Home Server and,
|
||||||
|
* optionally, the identity servers.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* add threepid API on the homeserver.
|
||||||
|
*/
|
||||||
|
class AddThreepid {
|
||||||
|
constructor() {
|
||||||
|
this.clientSecret = MatrixClientPeg.get().generateClientSecret();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to add an email threepid. This will trigger a side-effect of
|
||||||
|
* sending an email to the provided email address.
|
||||||
|
* @param {string} emailAddress The email address to add
|
||||||
|
* @param {boolean} bind If True, bind this email to this mxid on the Identity Server
|
||||||
|
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
|
||||||
|
*/
|
||||||
|
addEmailAddress(emailAddress, bind) {
|
||||||
|
this.bind = bind;
|
||||||
|
return MatrixClientPeg.get().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 add the threepid
|
||||||
|
* @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() {
|
||||||
|
var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
|
return MatrixClientPeg.get().addThreePid({
|
||||||
|
sid: this.sessionId,
|
||||||
|
client_secret: this.clientSecret,
|
||||||
|
id_server: identityServerDomain
|
||||||
|
}, this.bind).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) {
|
||||||
|
err.message += ` (Status ${err.httpStatus})`;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AddThreepid;
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -37,6 +37,17 @@ module.exports = {
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
avatarUrlForUser: function(user, width, height, resizeMethod) {
|
||||||
|
var url = ContentRepo.getHttpUriForMxc(
|
||||||
|
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||||
|
width, height, resizeMethod
|
||||||
|
);
|
||||||
|
if (!url || url.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
|
||||||
defaultAvatarUrlForString: function(s) {
|
defaultAvatarUrlForString: function(s) {
|
||||||
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
|
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
|
||||||
var total = 0;
|
var total = 0;
|
||||||
|
|
107
src/Entities.js
Normal file
107
src/Entities.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
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 React = require('react');
|
||||||
|
var sdk = require('./index');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Converts various data models to Entity objects.
|
||||||
|
*
|
||||||
|
* Entity objects provide an interface for UI components to use to display
|
||||||
|
* members in a data-agnostic way. This means they don't need to care if the
|
||||||
|
* underlying data model is a RoomMember, User or 3PID data structure, it just
|
||||||
|
* cares about rendering.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Entity {
|
||||||
|
constructor(model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
getJsx() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(queryString) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemberEntity extends Entity {
|
||||||
|
getJsx() {
|
||||||
|
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||||
|
return (
|
||||||
|
<MemberTile key={this.model.userId} member={this.model} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(queryString) {
|
||||||
|
return this.model.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserEntity extends Entity {
|
||||||
|
|
||||||
|
constructor(model, showInviteButton, inviteFn) {
|
||||||
|
super(model);
|
||||||
|
this.showInviteButton = Boolean(showInviteButton);
|
||||||
|
this.inviteFn = inviteFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick() {
|
||||||
|
if (this.inviteFn) {
|
||||||
|
this.inviteFn(this.model.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getJsx() {
|
||||||
|
var UserTile = sdk.getComponent("rooms.UserTile");
|
||||||
|
return (
|
||||||
|
<UserTile key={this.model.userId} user={this.model}
|
||||||
|
showInviteButton={this.showInviteButton} onClick={this.onClick.bind(this)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(queryString) {
|
||||||
|
var name = this.model.displayName || this.model.userId;
|
||||||
|
return name.toLowerCase().indexOf(queryString.toLowerCase()) === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* @param {RoomMember[]} members
|
||||||
|
* @return {Entity[]}
|
||||||
|
*/
|
||||||
|
fromRoomMembers: function(members) {
|
||||||
|
return members.map(function(m) {
|
||||||
|
return new MemberEntity(m);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {User[]} users
|
||||||
|
* @param {boolean} showInviteButton
|
||||||
|
* @param {Function} inviteFn Called with the user ID.
|
||||||
|
* @return {Entity[]}
|
||||||
|
*/
|
||||||
|
fromUsers: function(users, showInviteButton, inviteFn) {
|
||||||
|
return users.map(function(u) {
|
||||||
|
return new UserEntity(u, showInviteButton, inviteFn);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
51
src/GuestAccess.js
Normal file
51
src/GuestAccess.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
const IS_GUEST_KEY = "matrix-is-guest";
|
||||||
|
|
||||||
|
class GuestAccess {
|
||||||
|
|
||||||
|
constructor(localStorage) {
|
||||||
|
this.localStorage = localStorage;
|
||||||
|
try {
|
||||||
|
this._isGuest = localStorage.getItem(IS_GUEST_KEY) === "true";
|
||||||
|
}
|
||||||
|
catch (e) {} // don't care
|
||||||
|
}
|
||||||
|
|
||||||
|
setPeekedRoom(roomId) {
|
||||||
|
// we purposefully do not persist this to local storage as peeking is
|
||||||
|
// entirely transient.
|
||||||
|
this._peekedRoomId = roomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeekedRoom() {
|
||||||
|
return this._peekedRoomId;
|
||||||
|
}
|
||||||
|
|
||||||
|
isGuest() {
|
||||||
|
return this._isGuest;
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsGuest(isGuest) {
|
||||||
|
try {
|
||||||
|
this.localStorage.setItem(IS_GUEST_KEY, JSON.stringify(isGuest));
|
||||||
|
} catch (e) {} // ignore. If they don't do LS, they'll just get a new account.
|
||||||
|
this._isGuest = isGuest;
|
||||||
|
this._peekedRoomId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GuestAccess;
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
// A thing that holds your Matrix Client
|
// A thing that holds your Matrix Client
|
||||||
var Matrix = require("matrix-js-sdk");
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
var GuestAccess = require("./GuestAccess");
|
||||||
|
|
||||||
var matrixClient = null;
|
var matrixClient = null;
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ function deviceId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createClient(hs_url, is_url, user_id, access_token) {
|
function createClient(hs_url, is_url, user_id, access_token, guestAccess) {
|
||||||
var opts = {
|
var opts = {
|
||||||
baseUrl: hs_url,
|
baseUrl: hs_url,
|
||||||
idBaseUrl: is_url,
|
idBaseUrl: is_url,
|
||||||
|
@ -47,6 +48,15 @@ function createClient(hs_url, is_url, user_id, access_token) {
|
||||||
}
|
}
|
||||||
|
|
||||||
matrixClient = Matrix.createClient(opts);
|
matrixClient = Matrix.createClient(opts);
|
||||||
|
if (guestAccess) {
|
||||||
|
console.log("Guest: %s", guestAccess.isGuest());
|
||||||
|
matrixClient.setGuest(guestAccess.isGuest());
|
||||||
|
var peekedRoomId = guestAccess.getPeekedRoom();
|
||||||
|
if (peekedRoomId) {
|
||||||
|
console.log("Peeking in room %s", peekedRoomId);
|
||||||
|
matrixClient.peekInRoom(peekedRoomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
|
@ -54,12 +64,18 @@ if (localStorage) {
|
||||||
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||||
var access_token = localStorage.getItem("mx_access_token");
|
var access_token = localStorage.getItem("mx_access_token");
|
||||||
var user_id = localStorage.getItem("mx_user_id");
|
var user_id = localStorage.getItem("mx_user_id");
|
||||||
|
var guestAccess = new GuestAccess(localStorage);
|
||||||
if (access_token && user_id && hs_url) {
|
if (access_token && user_id && hs_url) {
|
||||||
createClient(hs_url, is_url, user_id, access_token);
|
createClient(hs_url, is_url, user_id, access_token, guestAccess);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MatrixClient {
|
class MatrixClient {
|
||||||
|
|
||||||
|
constructor(guestAccess) {
|
||||||
|
this.guestAccess = guestAccess;
|
||||||
|
}
|
||||||
|
|
||||||
get() {
|
get() {
|
||||||
return matrixClient;
|
return matrixClient;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +113,7 @@ class MatrixClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceUsingAccessToken(hs_url, is_url, user_id, access_token) {
|
replaceUsingAccessToken(hs_url, is_url, user_id, access_token, isGuest) {
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
@ -105,7 +121,8 @@ class MatrixClient {
|
||||||
console.warn("Error using local storage");
|
console.warn("Error using local storage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createClient(hs_url, is_url, user_id, access_token);
|
this.guestAccess.markAsGuest(Boolean(isGuest));
|
||||||
|
createClient(hs_url, is_url, user_id, access_token, this.guestAccess);
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("mx_hs_url", hs_url);
|
localStorage.setItem("mx_hs_url", hs_url);
|
||||||
|
@ -122,6 +139,6 @@ class MatrixClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.mxMatrixClient) {
|
if (!global.mxMatrixClient) {
|
||||||
global.mxMatrixClient = new MatrixClient();
|
global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage));
|
||||||
}
|
}
|
||||||
module.exports = global.mxMatrixClient;
|
module.exports = global.mxMatrixClient;
|
||||||
|
|
92
src/PasswordReset.js
Normal file
92
src/PasswordReset.js
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
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 = this.client.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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PasswordReset;
|
|
@ -73,6 +73,11 @@ class Presence {
|
||||||
}
|
}
|
||||||
var old_state = this.state;
|
var old_state = this.state;
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
|
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
return; // don't try to set presence when a guest; it won't work.
|
||||||
|
}
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||||
console.log("Presence: %s", newState);
|
console.log("Presence: %s", newState);
|
||||||
|
|
|
@ -69,6 +69,10 @@ class Register extends Signup {
|
||||||
this.params.idSid = idSid;
|
this.params.idSid = idSid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGuestAccessToken(token) {
|
||||||
|
this.guestAccessToken = token;
|
||||||
|
}
|
||||||
|
|
||||||
getStep() {
|
getStep() {
|
||||||
return this._step;
|
return this._step;
|
||||||
}
|
}
|
||||||
|
@ -126,7 +130,8 @@ class Register extends Signup {
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().register(
|
return MatrixClientPeg.get().register(
|
||||||
this.username, this.password, this.params.sessionId, authDict, bindEmail
|
this.username, this.password, this.params.sessionId, authDict, bindEmail,
|
||||||
|
this.guestAccessToken
|
||||||
).then(function(result) {
|
).then(function(result) {
|
||||||
self.credentials = result;
|
self.credentials = result;
|
||||||
self.setStep("COMPLETE");
|
self.setStep("COMPLETE");
|
||||||
|
@ -147,6 +152,8 @@ class Register extends Signup {
|
||||||
} else {
|
} else {
|
||||||
if (error.errcode === 'M_USER_IN_USE') {
|
if (error.errcode === 'M_USER_IN_USE') {
|
||||||
throw new Error("Username in use");
|
throw new Error("Username in use");
|
||||||
|
} 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) {
|
} else if (error.httpStatus == 401) {
|
||||||
throw new Error("Authorisation failed!");
|
throw new Error("Authorisation failed!");
|
||||||
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
||||||
|
|
|
@ -18,6 +18,32 @@ var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var MatrixTools = require("./MatrixTools");
|
var MatrixTools = require("./MatrixTools");
|
||||||
var dis = require("./dispatcher");
|
var dis = require("./dispatcher");
|
||||||
var encryption = require("./encryption");
|
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) {
|
var reject = function(msg) {
|
||||||
return {
|
return {
|
||||||
|
@ -33,16 +59,37 @@ var success = function(promise) {
|
||||||
|
|
||||||
var commands = {
|
var commands = {
|
||||||
// Change your nickname
|
// Change your nickname
|
||||||
nick: function(room_id, args) {
|
nick: new Command("nick", "<display_name>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setDisplayName(args)
|
MatrixClientPeg.get().setDisplayName(args)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return reject("Usage: /nick <display_name>");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
encrypt: function(room_id, args) {
|
// Changes the colorscheme of your current room
|
||||||
|
tint: new Command("tint", "<color1> [<color2>]", 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) {
|
||||||
|
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, "org.matrix.room.color_scheme", colorScheme
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(this.getUsage());
|
||||||
|
}),
|
||||||
|
|
||||||
|
encrypt: new Command("encrypt", "<on|off>", function(room_id, args) {
|
||||||
if (args == "on") {
|
if (args == "on") {
|
||||||
var client = MatrixClientPeg.get();
|
var client = MatrixClientPeg.get();
|
||||||
var members = client.getRoom(room_id).currentState.members;
|
var members = client.getRoom(room_id).currentState.members;
|
||||||
|
@ -58,21 +105,21 @@ var commands = {
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
return reject("Usage: encrypt <on/off>");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Change the room topic
|
// Change the room topic
|
||||||
topic: function(room_id, args) {
|
topic: new Command("topic", "<topic>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
return success(
|
return success(
|
||||||
MatrixClientPeg.get().setRoomTopic(room_id, args)
|
MatrixClientPeg.get().setRoomTopic(room_id, args)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return reject("Usage: /topic <topic>");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Invite a user
|
// Invite a user
|
||||||
invite: function(room_id, args) {
|
invite: new Command("invite", "<userId>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
var matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
@ -81,11 +128,11 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /invite <userId>");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Join a room
|
// Join a room
|
||||||
join: function(room_id, args) {
|
join: new Command("join", "<room_alias>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
var matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
@ -94,8 +141,7 @@ var commands = {
|
||||||
return reject("Usage: /join #alias:domain");
|
return reject("Usage: /join #alias:domain");
|
||||||
}
|
}
|
||||||
if (!room_alias.match(/:/)) {
|
if (!room_alias.match(/:/)) {
|
||||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
room_alias += ':' + domain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a room with this alias
|
// Try to find a room with this alias
|
||||||
|
@ -128,21 +174,20 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /join <room_alias>");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
part: function(room_id, args) {
|
part: new Command("part", "[#alias:domain]", function(room_id, args) {
|
||||||
var targetRoomId;
|
var targetRoomId;
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
var matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
var room_alias = matches[1];
|
var room_alias = matches[1];
|
||||||
if (room_alias[0] !== '#') {
|
if (room_alias[0] !== '#') {
|
||||||
return reject("Usage: /part [#alias:domain]");
|
return reject(this.getUsage());
|
||||||
}
|
}
|
||||||
if (!room_alias.match(/:/)) {
|
if (!room_alias.match(/:/)) {
|
||||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
||||||
room_alias += ':' + domain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find a room with this alias
|
// Try to find a room with this alias
|
||||||
|
@ -175,10 +220,10 @@ var commands = {
|
||||||
dis.dispatch({action: 'view_next_room'});
|
dis.dispatch({action: 'view_next_room'});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Kick a user from the room with an optional reason
|
// Kick a user from the room with an optional reason
|
||||||
kick: function(room_id, args) {
|
kick: new Command("kick", "<userId> [<reason>]", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
@ -187,11 +232,11 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /kick <userId> [<reason>]");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Ban a user from the room with an optional reason
|
// Ban a user from the room with an optional reason
|
||||||
ban: function(room_id, args) {
|
ban: new Command("ban", "<userId> [<reason>]", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
@ -200,11 +245,11 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /ban <userId> [<reason>]");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Unban a user from the room
|
// Unban a user from the room
|
||||||
unban: function(room_id, args) {
|
unban: new Command("unban", "<userId>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
var matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
@ -214,11 +259,11 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /unban <userId>");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
op: function(room_id, args) {
|
op: new Command("op", "<userId> [<power level>]", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+?)( +(\d+))?$/);
|
var matches = args.match(/^(\S+?)( +(\d+))?$/);
|
||||||
var powerLevel = 50; // default power level for op
|
var powerLevel = 50; // default power level for op
|
||||||
|
@ -243,11 +288,11 @@ var commands = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /op <userId> [<power level>]");
|
return reject(this.getUsage());
|
||||||
},
|
}),
|
||||||
|
|
||||||
// Reset the power level of a user
|
// Reset the power level of a user
|
||||||
deop: function(room_id, args) {
|
deop: new Command("deop", "<userId>", function(room_id, args) {
|
||||||
if (args) {
|
if (args) {
|
||||||
var matches = args.match(/^(\S+)$/);
|
var matches = args.match(/^(\S+)$/);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
|
@ -266,12 +311,14 @@ var commands = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reject("Usage: /deop <userId>");
|
return reject(this.getUsage());
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// helpful aliases
|
// helpful aliases
|
||||||
commands.j = commands.join;
|
var aliases = {
|
||||||
|
j: "join"
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
/**
|
/**
|
||||||
|
@ -291,13 +338,26 @@ module.exports = {
|
||||||
var cmd = bits[1].substring(1).toLowerCase();
|
var cmd = bits[1].substring(1).toLowerCase();
|
||||||
var args = bits[3];
|
var args = bits[3];
|
||||||
if (cmd === "me") return null;
|
if (cmd === "me") return null;
|
||||||
|
if (aliases[cmd]) {
|
||||||
|
cmd = aliases[cmd];
|
||||||
|
}
|
||||||
if (commands[cmd]) {
|
if (commands[cmd]) {
|
||||||
return commands[cmd](roomId, args);
|
return commands[cmd].run(roomId, args);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return reject("Unrecognised command: " + input);
|
return reject("Unrecognised command: " + input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // not a command
|
return null; // not a command
|
||||||
|
},
|
||||||
|
|
||||||
|
getCommandList: function() {
|
||||||
|
// 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", "<action>", function(){}));
|
||||||
|
|
||||||
|
return cmds;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,8 +32,6 @@ const MATCH_REGEX = /(^|\s)(\S+)$/;
|
||||||
class TabComplete {
|
class TabComplete {
|
||||||
|
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
opts.startingWordSuffix = opts.startingWordSuffix || "";
|
|
||||||
opts.wordSuffix = opts.wordSuffix || "";
|
|
||||||
opts.allowLooping = opts.allowLooping || false;
|
opts.allowLooping = opts.allowLooping || false;
|
||||||
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
|
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
|
||||||
opts.onClickCompletes = opts.onClickCompletes || false;
|
opts.onClickCompletes = opts.onClickCompletes || false;
|
||||||
|
@ -58,7 +56,7 @@ class TabComplete {
|
||||||
// assign onClick listeners for each entry to complete the text
|
// assign onClick listeners for each entry to complete the text
|
||||||
this.list.forEach((l) => {
|
this.list.forEach((l) => {
|
||||||
l.onClick = () => {
|
l.onClick = () => {
|
||||||
this.completeTo(l.getText());
|
this.completeTo(l);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -93,10 +91,12 @@ class TabComplete {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do an auto-complete with the given word. This terminates the tab-complete.
|
* 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) {
|
completeTo(entry) {
|
||||||
this.textArea.value = this._replaceWith(someVal, true);
|
this.textArea.value = this._replaceWith(
|
||||||
|
entry.getFillText(), true, entry.getSuffix(this.isFirstWord)
|
||||||
|
);
|
||||||
this.stopTabCompleting();
|
this.stopTabCompleting();
|
||||||
// keep focus on the text area
|
// keep focus on the text area
|
||||||
this.textArea.focus();
|
this.textArea.focus();
|
||||||
|
@ -222,8 +222,9 @@ class TabComplete {
|
||||||
if (!this.inPassiveMode) {
|
if (!this.inPassiveMode) {
|
||||||
// set textarea to this new value
|
// set textarea to this new value
|
||||||
this.textArea.value = this._replaceWith(
|
this.textArea.value = this._replaceWith(
|
||||||
this.matchedList[this.currentIndex].text,
|
this.matchedList[this.currentIndex].getFillText(),
|
||||||
this.currentIndex !== 0 // don't suffix the original text!
|
this.currentIndex !== 0, // don't suffix the original text!
|
||||||
|
this.matchedList[this.currentIndex].getSuffix(this.isFirstWord)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +244,7 @@ class TabComplete {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_replaceWith(newVal, includeSuffix) {
|
_replaceWith(newVal, includeSuffix, suffix) {
|
||||||
// The regex to replace the input matches a character of whitespace AND
|
// 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
|
// 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
|
// replace the partial word AND the character of whitespace. We want to
|
||||||
|
@ -258,13 +259,12 @@ class TabComplete {
|
||||||
boundaryChar = "";
|
boundaryChar = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
var replacementText = (
|
suffix = suffix || "";
|
||||||
boundaryChar + newVal + (
|
if (!includeSuffix) {
|
||||||
includeSuffix ?
|
suffix = "";
|
||||||
(this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix) :
|
}
|
||||||
""
|
|
||||||
)
|
var replacementText = boundaryChar + newVal + suffix;
|
||||||
);
|
|
||||||
return this.originalText.replace(MATCH_REGEX, function() {
|
return this.originalText.replace(MATCH_REGEX, function() {
|
||||||
return replacementText; // function form to avoid `$` special-casing
|
return replacementText; // function form to avoid `$` special-casing
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,14 @@ class Entry {
|
||||||
return this.text;
|
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
|
* @return {ReactClass} Raw JSX
|
||||||
*/
|
*/
|
||||||
|
@ -42,6 +50,14 @@ class Entry {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {?string} The suffix to append to the tab-complete, or null to
|
||||||
|
* not do this.
|
||||||
|
*/
|
||||||
|
getSuffix(isFirstWord) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when this entry is clicked.
|
* Called when this entry is clicked.
|
||||||
*/
|
*/
|
||||||
|
@ -50,6 +66,31 @@ class Entry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CommandEntry extends Entry {
|
||||||
|
constructor(cmd, cmdWithArgs) {
|
||||||
|
super(cmdWithArgs);
|
||||||
|
this.cmd = cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFillText() {
|
||||||
|
return this.cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey() {
|
||||||
|
return this.getFillText();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuffix(isFirstWord) {
|
||||||
|
return " "; // force a space after the command.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandEntry.fromCommands = function(commandArray) {
|
||||||
|
return commandArray.map(function(cmd) {
|
||||||
|
return new CommandEntry(cmd.getCommand(), cmd.getCommandWithArgs());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class MemberEntry extends Entry {
|
class MemberEntry extends Entry {
|
||||||
constructor(member) {
|
constructor(member) {
|
||||||
super(member.name || member.userId);
|
super(member.name || member.userId);
|
||||||
|
@ -66,6 +107,10 @@ class MemberEntry extends Entry {
|
||||||
getKey() {
|
getKey() {
|
||||||
return this.member.userId;
|
return this.member.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSuffix(isFirstWord) {
|
||||||
|
return isFirstWord ? ": " : " ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MemberEntry.fromMemberList = function(members) {
|
MemberEntry.fromMemberList = function(members) {
|
||||||
|
@ -99,3 +144,4 @@ MemberEntry.fromMemberList = function(members) {
|
||||||
|
|
||||||
module.exports.Entry = Entry;
|
module.exports.Entry = Entry;
|
||||||
module.exports.MemberEntry = MemberEntry;
|
module.exports.MemberEntry = MemberEntry;
|
||||||
|
module.exports.CommandEntry = CommandEntry;
|
||||||
|
|
|
@ -66,7 +66,7 @@ function textForMemberEvent(ev) {
|
||||||
function textForTopicEvent(ev) {
|
function textForTopicEvent(ev) {
|
||||||
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
return senderDisplayName + ' changed the topic to, "' + ev.getContent().topic + '"';
|
return senderDisplayName + ' changed the topic to "' + ev.getContent().topic + '"';
|
||||||
};
|
};
|
||||||
|
|
||||||
function textForRoomNameEvent(ev) {
|
function textForRoomNameEvent(ev) {
|
||||||
|
|
211
src/Tinter.js
Normal file
211
src/Tinter.js
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var dis = require("./dispatcher");
|
||||||
|
|
||||||
|
// FIXME: these vars should be bundled up and attached to
|
||||||
|
// module.exports otherwise this will break when included by both
|
||||||
|
// react-sdk and apps layered on top.
|
||||||
|
|
||||||
|
// The colour keys to be replaced as referred to in SVGs
|
||||||
|
var keyRgb = [
|
||||||
|
"rgb(118, 207, 166)", // Vector Green
|
||||||
|
"rgb(234, 245, 240)", // Vector Light Green
|
||||||
|
"rgba(118, 207, 166, 0.2)", // BottomLeftMenu overlay (20% Vector Green)
|
||||||
|
];
|
||||||
|
|
||||||
|
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
||||||
|
// x * 118 + (1 - x) * 255 = 234
|
||||||
|
// x * 118 + 255 - 255 * x = 234
|
||||||
|
// x * 118 - x * 255 = 234 - 255
|
||||||
|
// (255 - 118) x = 255 - 234
|
||||||
|
// x = (255 - 234) / (255 - 118) = 0.16
|
||||||
|
|
||||||
|
// The colour keys to be replaced as referred to in SVGs
|
||||||
|
var keyHex = [
|
||||||
|
"#76CFA6", // Vector Green
|
||||||
|
"#EAF5F0", // Vector Light Green
|
||||||
|
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green)
|
||||||
|
];
|
||||||
|
|
||||||
|
// cache of our replacement colours
|
||||||
|
// defaults to our keys.
|
||||||
|
var colors = [
|
||||||
|
keyHex[0],
|
||||||
|
keyHex[1],
|
||||||
|
keyHex[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
var cssFixups = [
|
||||||
|
// {
|
||||||
|
// style: a style object that should be fixed up taken from a stylesheet
|
||||||
|
// attr: name of the attribute to be clobbered, e.g. 'color'
|
||||||
|
// index: ordinal of primary, secondary or tertiary
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
// CSS attributes to be fixed up
|
||||||
|
var cssAttrs = [
|
||||||
|
"color",
|
||||||
|
"backgroundColor",
|
||||||
|
"borderColor",
|
||||||
|
"borderTopColor",
|
||||||
|
"borderBottomColor",
|
||||||
|
];
|
||||||
|
|
||||||
|
var svgAttrs = [
|
||||||
|
"fill",
|
||||||
|
"stroke",
|
||||||
|
];
|
||||||
|
|
||||||
|
var cached = false;
|
||||||
|
|
||||||
|
function calcCssFixups() {
|
||||||
|
for (var i = 0; i < document.styleSheets.length; i++) {
|
||||||
|
var ss = document.styleSheets[i];
|
||||||
|
for (var j = 0; j < ss.cssRules.length; j++) {
|
||||||
|
var rule = ss.cssRules[j];
|
||||||
|
if (!rule.style) continue;
|
||||||
|
for (var k = 0; k < cssAttrs.length; k++) {
|
||||||
|
var attr = cssAttrs[k];
|
||||||
|
for (var l = 0; l < keyRgb.length; l++) {
|
||||||
|
if (rule.style[attr] === keyRgb[l]) {
|
||||||
|
cssFixups.push({
|
||||||
|
style: rule.style,
|
||||||
|
attr: attr,
|
||||||
|
index: l,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCssFixups() {
|
||||||
|
for (var i = 0; i < cssFixups.length; i++) {
|
||||||
|
var cssFixup = cssFixups[i];
|
||||||
|
cssFixup.style[cssFixup.attr] = colors[cssFixup.index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgb(color) {
|
||||||
|
if (color[0] === '#') color = color.slice(1);
|
||||||
|
if (color.length === 3) {
|
||||||
|
color = color[0] + color[0] +
|
||||||
|
color[1] + color[1] +
|
||||||
|
color[2] + color[2];
|
||||||
|
}
|
||||||
|
var val = parseInt(color, 16);
|
||||||
|
var r = (val >> 16) & 255;
|
||||||
|
var g = (val >> 8) & 255;
|
||||||
|
var b = val & 255;
|
||||||
|
return [r, g, b];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(rgb) {
|
||||||
|
var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
||||||
|
return '#' + (0x1000000 + val).toString(16).slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tint: function(primaryColor, secondaryColor, tertiaryColor) {
|
||||||
|
if (!cached) {
|
||||||
|
calcCssFixups();
|
||||||
|
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);
|
||||||
|
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
||||||
|
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
||||||
|
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
||||||
|
secondaryColor = rgbToHex(rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tertiaryColor) {
|
||||||
|
var x = 0.19;
|
||||||
|
var rgb1 = hexToRgb(primaryColor);
|
||||||
|
var rgb2 = hexToRgb(secondaryColor);
|
||||||
|
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
||||||
|
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
||||||
|
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
||||||
|
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.
|
||||||
|
applyCssFixups();
|
||||||
|
|
||||||
|
// tell all the SVGs to go fix themselves up
|
||||||
|
dis.dispatch({ action: 'tint_update' });
|
||||||
|
},
|
||||||
|
|
||||||
|
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
||||||
|
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
||||||
|
// keeping it here for now.
|
||||||
|
calcSvgFixups: function(svgs) {
|
||||||
|
// go through manually fixing up SVG colours.
|
||||||
|
// we could do this by stylesheets, but keeping the stylesheets
|
||||||
|
// updated would be a PITA, so just brute-force search for the
|
||||||
|
// key colour; cache the element and apply.
|
||||||
|
|
||||||
|
var fixups = [];
|
||||||
|
for (var i = 0; i < svgs.length; i++) {
|
||||||
|
var svgDoc = svgs[i].contentDocument;
|
||||||
|
if (!svgDoc) continue;
|
||||||
|
var tags = svgDoc.getElementsByTagName("*");
|
||||||
|
for (var j = 0; j < tags.length; j++) {
|
||||||
|
var tag = tags[j];
|
||||||
|
for (var k = 0; k < svgAttrs.length; k++) {
|
||||||
|
var attr = svgAttrs[k];
|
||||||
|
for (var l = 0; l < keyHex.length; l++) {
|
||||||
|
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) {
|
||||||
|
fixups.push({
|
||||||
|
node: tag,
|
||||||
|
attr: attr,
|
||||||
|
index: l,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixups;
|
||||||
|
},
|
||||||
|
|
||||||
|
applySvgFixups: function(fixups) {
|
||||||
|
for (var i = 0; i < fixups.length; i++) {
|
||||||
|
var svgFixup = fixups[i];
|
||||||
|
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
|
@ -16,7 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
var dis = require("./dispatcher");
|
var dis = require("./dispatcher");
|
||||||
|
|
||||||
var MIN_DISPATCH_INTERVAL = 1 * 1000;
|
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)
|
* This class watches for user activity (moving the mouse or pressing a key)
|
||||||
|
@ -31,8 +32,14 @@ class UserActivity {
|
||||||
start() {
|
start() {
|
||||||
document.onmousemove = this._onUserActivity.bind(this);
|
document.onmousemove = this._onUserActivity.bind(this);
|
||||||
document.onkeypress = this._onUserActivity.bind(this);
|
document.onkeypress = this._onUserActivity.bind(this);
|
||||||
|
// can't use document.scroll here because that's only the document
|
||||||
|
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||||
|
// also this needs to be the wheel event, not scroll, as scroll is
|
||||||
|
// fired when the view scrolls down for a new message.
|
||||||
|
window.addEventListener('wheel', this._onUserActivity.bind(this), true);
|
||||||
this.lastActivityAtTs = new Date().getTime();
|
this.lastActivityAtTs = new Date().getTime();
|
||||||
this.lastDispatchAtTs = 0;
|
this.lastDispatchAtTs = 0;
|
||||||
|
this.activityEndTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,10 +48,19 @@ class UserActivity {
|
||||||
stop() {
|
stop() {
|
||||||
document.onmousemove = undefined;
|
document.onmousemove = undefined;
|
||||||
document.onkeypress = undefined;
|
document.onkeypress = undefined;
|
||||||
|
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_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserActivity(event) {
|
_onUserActivity(event) {
|
||||||
if (event.screenX) {
|
if (event.screenX && event.type == "mousemove") {
|
||||||
if (event.screenX === this.lastScreenX &&
|
if (event.screenX === this.lastScreenX &&
|
||||||
event.screenY === this.lastScreenY)
|
event.screenY === this.lastScreenY)
|
||||||
{
|
{
|
||||||
|
@ -55,12 +71,32 @@ class UserActivity {
|
||||||
this.lastScreenY = event.screenY;
|
this.lastScreenY = event.screenY;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastActivityAtTs = (new Date).getTime();
|
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;
|
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'user_activity'
|
action: 'user_activity'
|
||||||
});
|
});
|
||||||
|
if (!this.activityEndTimer) {
|
||||||
|
this.activityEndTimer = setTimeout(
|
||||||
|
this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onActivityEndTimer() {
|
||||||
|
var now = new Date().getTime();
|
||||||
|
var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS;
|
||||||
|
if (now >= targetTime) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'user_activity_end'
|
||||||
|
});
|
||||||
|
this.activityEndTimer = undefined;
|
||||||
|
} else {
|
||||||
|
this.activityEndTimer = setTimeout(
|
||||||
|
this._onActivityEndTimer.bind(this), targetTime - now
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
var q = require("q");
|
||||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
var Notifier = require("./Notifier");
|
var Notifier = require("./Notifier");
|
||||||
|
|
||||||
|
@ -35,6 +35,11 @@ module.exports = {
|
||||||
},
|
},
|
||||||
|
|
||||||
loadThreePids: function() {
|
loadThreePids: function() {
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
return q({
|
||||||
|
threepids: []
|
||||||
|
}); // guests can't poke 3pid endpoint
|
||||||
|
}
|
||||||
return MatrixClientPeg.get().getThreePids();
|
return MatrixClientPeg.get().getThreePids();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
||||||
var old = oldChildren[c.key];
|
var old = oldChildren[c.key];
|
||||||
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
var oldNode = ReactDom.findDOMNode(self.nodes[old.key]);
|
||||||
|
|
||||||
if (oldNode.style.left != c.props.style.left) {
|
if (oldNode && oldNode.style.left != c.props.style.left) {
|
||||||
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() {
|
||||||
// special case visibility because it's nonsensical to animate an invisible element
|
// special case visibility because it's nonsensical to animate an invisible element
|
||||||
// so we always hidden->visible pre-transition and visible->hidden after
|
// so we always hidden->visible pre-transition and visible->hidden after
|
||||||
|
@ -73,6 +73,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
collectNode: function(k, node) {
|
collectNode: function(k, node) {
|
||||||
if (
|
if (
|
||||||
|
node &&
|
||||||
this.nodes[k] === undefined &&
|
this.nodes[k] === undefined &&
|
||||||
node.props.startStyle &&
|
node.props.startStyle &&
|
||||||
Object.keys(node.props.startStyle).length
|
Object.keys(node.props.startStyle).length
|
||||||
|
|
|
@ -31,6 +31,11 @@ module.exports.components['structures.RoomView'] = require('./components/structu
|
||||||
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
|
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
|
||||||
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
|
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
|
||||||
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
|
||||||
|
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['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar');
|
||||||
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
|
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.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
|
||||||
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
|
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
|
||||||
|
@ -39,8 +44,11 @@ module.exports.components['views.create_room.RoomAlias'] = require('./components
|
||||||
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
|
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
|
||||||
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
|
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.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');
|
||||||
|
module.exports.components['views.dialogs.TextInputDialog'] = require('./components/views/dialogs/TextInputDialog');
|
||||||
module.exports.components['views.elements.EditableText'] = require('./components/views/elements/EditableText');
|
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.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');
|
module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector');
|
||||||
module.exports.components['views.login.CaptchaForm'] = require('./components/views/login/CaptchaForm');
|
module.exports.components['views.login.CaptchaForm'] = require('./components/views/login/CaptchaForm');
|
||||||
module.exports.components['views.login.CasLogin'] = require('./components/views/login/CasLogin');
|
module.exports.components['views.login.CasLogin'] = require('./components/views/login/CasLogin');
|
||||||
|
@ -57,17 +65,22 @@ module.exports.components['views.messages.MVideoBody'] = require('./components/v
|
||||||
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
|
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.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
||||||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
|
||||||
|
module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile');
|
||||||
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile');
|
||||||
module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo');
|
module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo');
|
||||||
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
|
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.MemberTile'] = require('./components/views/rooms/MemberTile');
|
||||||
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
|
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.RoomHeader'] = require('./components/views/rooms/RoomHeader');
|
||||||
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
|
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.RoomSettings'] = require('./components/views/rooms/RoomSettings');
|
||||||
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile');
|
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile');
|
||||||
|
module.exports.components['views.rooms.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList');
|
||||||
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
|
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile');
|
||||||
module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar');
|
module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar');
|
||||||
|
module.exports.components['views.rooms.UserTile'] = require('./components/views/rooms/UserTile');
|
||||||
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
module.exports.components['views.settings.ChangeAvatar'] = require('./components/views/settings/ChangeAvatar');
|
||||||
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
module.exports.components['views.settings.ChangeDisplayName'] = require('./components/views/settings/ChangeDisplayName');
|
||||||
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
module.exports.components['views.settings.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
||||||
|
|
|
@ -251,7 +251,7 @@ module.exports = React.createClass({
|
||||||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||||
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
||||||
|
|
||||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
var domain = MatrixClientPeg.get().getDomain();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CreateRoom">
|
<div className="mx_CreateRoom">
|
||||||
|
|
|
@ -31,6 +31,7 @@ var Registration = require("./login/Registration");
|
||||||
var PostRegistration = require("./login/PostRegistration");
|
var PostRegistration = require("./login/PostRegistration");
|
||||||
|
|
||||||
var Modal = require("../../Modal");
|
var Modal = require("../../Modal");
|
||||||
|
var Tinter = require("../../Tinter");
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var MatrixTools = require('../../MatrixTools');
|
var MatrixTools = require('../../MatrixTools');
|
||||||
var linkifyMatrix = require("../../linkify-matrix");
|
var linkifyMatrix = require("../../linkify-matrix");
|
||||||
|
@ -43,6 +44,7 @@ module.exports = React.createClass({
|
||||||
ConferenceHandler: React.PropTypes.any,
|
ConferenceHandler: React.PropTypes.any,
|
||||||
onNewScreen: React.PropTypes.func,
|
onNewScreen: React.PropTypes.func,
|
||||||
registrationUrl: React.PropTypes.string,
|
registrationUrl: React.PropTypes.string,
|
||||||
|
enableGuest: React.PropTypes.bool,
|
||||||
startingQueryParams: React.PropTypes.object
|
startingQueryParams: React.PropTypes.object
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -63,7 +65,8 @@ module.exports = React.createClass({
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
width: 10000
|
width: 10000,
|
||||||
|
autoPeek: true, // by default, we peek into rooms when we try to join them
|
||||||
};
|
};
|
||||||
if (s.logged_in) {
|
if (s.logged_in) {
|
||||||
if (MatrixClientPeg.get().getRooms().length) {
|
if (MatrixClientPeg.get().getRooms().length) {
|
||||||
|
@ -88,8 +91,21 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
|
this._autoRegisterAsGuest = false;
|
||||||
|
if (this.props.enableGuest) {
|
||||||
|
if (!this.props.config || !this.props.config.default_hs_url) {
|
||||||
|
console.error("Cannot enable guest access: No supplied config prop for HS/IS URLs");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._autoRegisterAsGuest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
if (this.state.logged_in) {
|
if (this.state.logged_in) {
|
||||||
|
// Don't auto-register as a guest. This applies if you refresh the page on a
|
||||||
|
// logged in client THEN hit the Sign Out button.
|
||||||
|
this._autoRegisterAsGuest = false;
|
||||||
this.startMatrixClient();
|
this.startMatrixClient();
|
||||||
}
|
}
|
||||||
this.focusComposer = false;
|
this.focusComposer = false;
|
||||||
|
@ -98,8 +114,11 @@ module.exports = React.createClass({
|
||||||
this.scrollStateMap = {};
|
this.scrollStateMap = {};
|
||||||
document.addEventListener("keydown", this.onKeyDown);
|
document.addEventListener("keydown", this.onKeyDown);
|
||||||
window.addEventListener("focus", this.onFocus);
|
window.addEventListener("focus", this.onFocus);
|
||||||
|
|
||||||
if (this.state.logged_in) {
|
if (this.state.logged_in) {
|
||||||
this.notifyNewScreen('');
|
this.notifyNewScreen('');
|
||||||
|
} else if (this._autoRegisterAsGuest) {
|
||||||
|
this._registerAsGuest();
|
||||||
} else {
|
} else {
|
||||||
this.notifyNewScreen('login');
|
this.notifyNewScreen('login');
|
||||||
}
|
}
|
||||||
|
@ -131,6 +150,34 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_registerAsGuest: function() {
|
||||||
|
var self = this;
|
||||||
|
var config = this.props.config;
|
||||||
|
console.log("Doing guest login on %s", config.default_hs_url);
|
||||||
|
MatrixClientPeg.replaceUsingUrls(
|
||||||
|
config.default_hs_url, config.default_is_url
|
||||||
|
);
|
||||||
|
MatrixClientPeg.get().registerGuest().done(function(creds) {
|
||||||
|
console.log("Registered as guest: %s", creds.user_id);
|
||||||
|
self._setAutoRegisterAsGuest(false);
|
||||||
|
self.onLoggedIn({
|
||||||
|
userId: creds.user_id,
|
||||||
|
accessToken: creds.access_token,
|
||||||
|
homeserverUrl: config.default_hs_url,
|
||||||
|
identityServerUrl: config.default_is_url,
|
||||||
|
guest: true
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
console.error(err.data);
|
||||||
|
self._setAutoRegisterAsGuest(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_setAutoRegisterAsGuest: function(shouldAutoRegister) {
|
||||||
|
this._autoRegisterAsGuest = shouldAutoRegister;
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
var roomIndexDelta = 1;
|
var roomIndexDelta = 1;
|
||||||
|
|
||||||
|
@ -185,6 +232,21 @@ module.exports = React.createClass({
|
||||||
screen: 'post_registration'
|
screen: 'post_registration'
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case 'start_upgrade_registration':
|
||||||
|
this.replaceState({
|
||||||
|
screen: "register",
|
||||||
|
upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(),
|
||||||
|
guestAccessToken: MatrixClientPeg.get().getAccessToken()
|
||||||
|
});
|
||||||
|
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':
|
case 'token_login':
|
||||||
if (this.state.logged_in) return;
|
if (this.state.logged_in) return;
|
||||||
|
|
||||||
|
@ -248,7 +310,10 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'view_room':
|
case 'view_room':
|
||||||
this._viewRoom(payload.room_id);
|
// by default we autoPeek rooms, unless we were called explicitly with
|
||||||
|
// autoPeek=false by something like RoomDirectory who has already peeked
|
||||||
|
this.setState({ autoPeek : payload.auto_peek === false ? false : true });
|
||||||
|
this._viewRoom(payload.room_id, payload.show_settings);
|
||||||
break;
|
break;
|
||||||
case 'view_prev_room':
|
case 'view_prev_room':
|
||||||
roomIndexDelta = -1;
|
roomIndexDelta = -1;
|
||||||
|
@ -301,8 +366,29 @@ module.exports = React.createClass({
|
||||||
this.notifyNewScreen('settings');
|
this.notifyNewScreen('settings');
|
||||||
break;
|
break;
|
||||||
case 'view_create_room':
|
case 'view_create_room':
|
||||||
this._setPage(this.PageTypes.CreateRoom);
|
//this._setPage(this.PageTypes.CreateRoom);
|
||||||
this.notifyNewScreen('new');
|
//this.notifyNewScreen('new');
|
||||||
|
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
var modal = Modal.createDialog(Loader);
|
||||||
|
|
||||||
|
MatrixClientPeg.get().createRoom({
|
||||||
|
preset: "private_chat"
|
||||||
|
}).done(function(res) {
|
||||||
|
modal.close();
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: res.room_id,
|
||||||
|
// show_settings: true,
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
modal.close();
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Failed to create room",
|
||||||
|
description: err.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'view_room_directory':
|
case 'view_room_directory':
|
||||||
this._setPage(this.PageTypes.RoomDirectory);
|
this._setPage(this.PageTypes.RoomDirectory);
|
||||||
|
@ -343,7 +429,7 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_viewRoom: function(roomId) {
|
_viewRoom: function(roomId, showSettings) {
|
||||||
// before we switch room, record the scroll state of the current room
|
// before we switch room, record the scroll state of the current room
|
||||||
this._updateScrollMap();
|
this._updateScrollMap();
|
||||||
|
|
||||||
|
@ -363,7 +449,16 @@ module.exports = React.createClass({
|
||||||
if (room) {
|
if (room) {
|
||||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||||
if (theAlias) presentedId = theAlias;
|
if (theAlias) presentedId = theAlias;
|
||||||
|
|
||||||
|
var color_scheme_event = room.getAccountData("org.matrix.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);
|
this.notifyNewScreen('room/'+presentedId);
|
||||||
newState.ready = true;
|
newState.ready = true;
|
||||||
}
|
}
|
||||||
|
@ -372,6 +467,9 @@ module.exports = React.createClass({
|
||||||
var scrollState = this.scrollStateMap[roomId];
|
var scrollState = this.scrollStateMap[roomId];
|
||||||
this.refs.roomView.restoreScrollState(scrollState);
|
this.refs.roomView.restoreScrollState(scrollState);
|
||||||
}
|
}
|
||||||
|
if (this.refs.roomView && showSettings) {
|
||||||
|
this.refs.roomView.showSettings(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// update scrollStateMap according to the current scroll state of the
|
// update scrollStateMap according to the current scroll state of the
|
||||||
|
@ -387,10 +485,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoggedIn: function(credentials) {
|
onLoggedIn: function(credentials) {
|
||||||
console.log("onLoggedIn => %s", credentials.userId);
|
credentials.guest = Boolean(credentials.guest);
|
||||||
|
console.log("onLoggedIn => %s (guest=%s)", credentials.userId, credentials.guest);
|
||||||
MatrixClientPeg.replaceUsingAccessToken(
|
MatrixClientPeg.replaceUsingAccessToken(
|
||||||
credentials.homeserverUrl, credentials.identityServerUrl,
|
credentials.homeserverUrl, credentials.identityServerUrl,
|
||||||
credentials.userId, credentials.accessToken
|
credentials.userId, credentials.accessToken, credentials.guest
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
screen: undefined,
|
screen: undefined,
|
||||||
|
@ -457,7 +556,9 @@ module.exports = React.createClass({
|
||||||
UserActivity.start();
|
UserActivity.start();
|
||||||
Presence.start();
|
Presence.start();
|
||||||
cli.startClient({
|
cli.startClient({
|
||||||
pendingEventOrdering: "end"
|
pendingEventOrdering: "end",
|
||||||
|
// deliberately huge limit for now to avoid hitting gappy /sync's until gappy /sync performance improves
|
||||||
|
initialSyncLimit: 250,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -511,6 +612,11 @@ module.exports = React.createClass({
|
||||||
action: 'token_login',
|
action: 'token_login',
|
||||||
params: params
|
params: params
|
||||||
});
|
});
|
||||||
|
} else if (screen == 'forgot_password') {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'start_password_recovery',
|
||||||
|
params: params
|
||||||
|
});
|
||||||
} else if (screen == 'new') {
|
} else if (screen == 'new') {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_create_room',
|
action: 'view_create_room',
|
||||||
|
@ -566,6 +672,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onUserClick: function(event, userId) {
|
onUserClick: function(event, userId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
/*
|
||||||
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||||
var member = new Matrix.RoomMember(null, userId);
|
var member = new Matrix.RoomMember(null, userId);
|
||||||
ContextualMenu.createMenu(MemberInfo, {
|
ContextualMenu.createMenu(MemberInfo, {
|
||||||
|
@ -573,6 +681,14 @@ module.exports = React.createClass({
|
||||||
right: window.innerWidth - event.pageX,
|
right: window.innerWidth - event.pageX,
|
||||||
top: event.pageY
|
top: event.pageY
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
var member = new Matrix.RoomMember(null, userId);
|
||||||
|
if (!member) { return; }
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_user',
|
||||||
|
member: member,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onLogoutClick: function(event) {
|
onLogoutClick: function(event) {
|
||||||
|
@ -620,10 +736,22 @@ module.exports = React.createClass({
|
||||||
this.showScreen("login");
|
this.showScreen("login");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onForgotPasswordClick: function() {
|
||||||
|
this.showScreen("forgot_password");
|
||||||
|
},
|
||||||
|
|
||||||
onRegistered: function(credentials) {
|
onRegistered: function(credentials) {
|
||||||
this.onLoggedIn(credentials);
|
this.onLoggedIn(credentials);
|
||||||
// do post-registration stuff
|
// do post-registration stuff
|
||||||
this.showScreen("post_registration");
|
// This now goes straight to user settings
|
||||||
|
// We use _setPage since if we wait for
|
||||||
|
// showScreen to do the dispatch loop,
|
||||||
|
// the showScreen dispatch will race with the
|
||||||
|
// sdk sync finishing and we'll probably see
|
||||||
|
// the page type still unset when the MatrixClient
|
||||||
|
// is started and show the Room Directory instead.
|
||||||
|
//this.showScreen("view_user_settings");
|
||||||
|
this._setPage(this.PageTypes.UserSettings);
|
||||||
},
|
},
|
||||||
|
|
||||||
onFinishPostRegistration: function() {
|
onFinishPostRegistration: function() {
|
||||||
|
@ -639,7 +767,9 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var rooms = MatrixClientPeg.get().getRooms();
|
var rooms = MatrixClientPeg.get().getRooms();
|
||||||
for (var i = 0; i < rooms.length; ++i) {
|
for (var i = 0; i < rooms.length; ++i) {
|
||||||
if (rooms[i].getUnreadNotificationCount()) {
|
if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) {
|
||||||
|
++notifCount;
|
||||||
|
} else if (rooms[i].getUnreadNotificationCount()) {
|
||||||
notifCount += rooms[i].getUnreadNotificationCount();
|
notifCount += rooms[i].getUnreadNotificationCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -671,6 +801,7 @@ module.exports = React.createClass({
|
||||||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
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
|
// needs to be before normal PageTypes as you are logged in technically
|
||||||
if (this.state.screen == 'post_registration') {
|
if (this.state.screen == 'post_registration') {
|
||||||
|
@ -689,6 +820,7 @@ module.exports = React.createClass({
|
||||||
<RoomView
|
<RoomView
|
||||||
ref="roomView"
|
ref="roomView"
|
||||||
roomId={this.state.currentRoom}
|
roomId={this.state.currentRoom}
|
||||||
|
autoPeek={this.state.autoPeek}
|
||||||
key={this.state.currentRoom}
|
key={this.state.currentRoom}
|
||||||
ConferenceHandler={this.props.ConferenceHandler} />
|
ConferenceHandler={this.props.ConferenceHandler} />
|
||||||
);
|
);
|
||||||
|
@ -734,12 +866,20 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (this.state.logged_in) {
|
} else if (this.state.logged_in || (!this.state.logged_in && this._autoRegisterAsGuest)) {
|
||||||
var Spinner = sdk.getComponent('elements.Spinner');
|
var Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
var logoutLink;
|
||||||
|
if (this.state.logged_in) {
|
||||||
|
logoutLink = (
|
||||||
|
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_MatrixChat_splash">
|
<div className="mx_MatrixChat_splash">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
|
{logoutLink}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.state.screen == 'register') {
|
} else if (this.state.screen == 'register') {
|
||||||
|
@ -749,19 +889,30 @@ module.exports = React.createClass({
|
||||||
sessionId={this.state.register_session_id}
|
sessionId={this.state.register_session_id}
|
||||||
idSid={this.state.register_id_sid}
|
idSid={this.state.register_id_sid}
|
||||||
email={this.props.startingQueryParams.email}
|
email={this.props.startingQueryParams.email}
|
||||||
|
username={this.state.upgradeUsername}
|
||||||
|
disableUsernameChanges={Boolean(this.state.upgradeUsername)}
|
||||||
|
guestAccessToken={this.state.guestAccessToken}
|
||||||
hsUrl={this.props.config.default_hs_url}
|
hsUrl={this.props.config.default_hs_url}
|
||||||
isUrl={this.props.config.default_is_url}
|
isUrl={this.props.config.default_is_url}
|
||||||
registrationUrl={this.props.registrationUrl}
|
registrationUrl={this.props.registrationUrl}
|
||||||
onLoggedIn={this.onRegistered}
|
onLoggedIn={this.onRegistered}
|
||||||
onLoginClick={this.onLoginClick} />
|
onLoginClick={this.onLoginClick} />
|
||||||
);
|
);
|
||||||
|
} else if (this.state.screen == 'forgot_password') {
|
||||||
|
return (
|
||||||
|
<ForgotPassword
|
||||||
|
homeserverUrl={this.props.config.default_hs_url}
|
||||||
|
identityServerUrl={this.props.config.default_is_url}
|
||||||
|
onComplete={this.onLoginClick} />
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Login
|
<Login
|
||||||
onLoggedIn={this.onLoggedIn}
|
onLoggedIn={this.onLoggedIn}
|
||||||
onRegisterClick={this.onRegisterClick}
|
onRegisterClick={this.onRegisterClick}
|
||||||
homeserverUrl={this.props.config.default_hs_url}
|
homeserverUrl={this.props.config.default_hs_url}
|
||||||
identityServerUrl={this.props.config.default_is_url} />
|
identityServerUrl={this.props.config.default_is_url}
|
||||||
|
onForgotPasswordClick={this.onForgotPasswordClick} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -112,6 +112,14 @@ module.exports = React.createClass({
|
||||||
this.checkFillState();
|
this.checkFillState();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
// set a boolean to say we've been unmounted, which any pending
|
||||||
|
// promises can use to throw away their results.
|
||||||
|
//
|
||||||
|
// (We could use isMounted(), but facebook have deprecated that.)
|
||||||
|
this.unmounted = true;
|
||||||
|
},
|
||||||
|
|
||||||
onScroll: function(ev) {
|
onScroll: function(ev) {
|
||||||
var sn = this._getScrollNode();
|
var sn = this._getScrollNode();
|
||||||
debuglog("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
debuglog("Scroll event: offset now:", sn.scrollTop, "recentEventScroll:", this.recentEventScroll);
|
||||||
|
@ -158,6 +166,10 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// check the scroll state and send out backfill requests if necessary.
|
// check the scroll state and send out backfill requests if necessary.
|
||||||
checkFillState: function() {
|
checkFillState: function() {
|
||||||
|
if (this.unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var sn = this._getScrollNode();
|
var sn = this._getScrollNode();
|
||||||
|
|
||||||
// if there is less than a screenful of messages above or below the
|
// if there is less than a screenful of messages above or below the
|
||||||
|
@ -346,6 +358,12 @@ module.exports = React.createClass({
|
||||||
* message panel.
|
* message panel.
|
||||||
*/
|
*/
|
||||||
_getScrollNode: function() {
|
_getScrollNode: function() {
|
||||||
|
if (this.unmounted) {
|
||||||
|
// this shouldn't happen, but when it does, turn the NPE into
|
||||||
|
// something more meaningful.
|
||||||
|
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||||
|
}
|
||||||
|
|
||||||
var panel = ReactDOM.findDOMNode(this.refs.geminiPanel);
|
var panel = ReactDOM.findDOMNode(this.refs.geminiPanel);
|
||||||
|
|
||||||
// If the gemini scrollbar is doing its thing, this will be a div within
|
// If the gemini scrollbar is doing its thing, this will be a div within
|
||||||
|
|
|
@ -57,7 +57,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!upload) {
|
if (!upload) {
|
||||||
upload = uploads[0];
|
return <div />
|
||||||
}
|
}
|
||||||
|
|
||||||
var innerProgressStyle = {
|
var innerProgressStyle = {
|
||||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
var ReactDOM = require('react-dom');
|
||||||
var sdk = require('../../index');
|
var sdk = require('../../index');
|
||||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||||
var Modal = require('../../Modal');
|
var Modal = require('../../Modal');
|
||||||
|
@ -21,6 +22,9 @@ var dis = require("../../dispatcher");
|
||||||
var q = require('q');
|
var q = require('q');
|
||||||
var version = require('../../../package.json').version;
|
var version = require('../../../package.json').version;
|
||||||
var UserSettingsStore = require('../../UserSettingsStore');
|
var UserSettingsStore = require('../../UserSettingsStore');
|
||||||
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
var Email = require('../../email');
|
||||||
|
var AddThreepid = require('../../AddThreepid');
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'UserSettings',
|
displayName: 'UserSettings',
|
||||||
|
@ -41,6 +45,7 @@ module.exports = React.createClass({
|
||||||
threePids: [],
|
threePids: [],
|
||||||
clientVersion: version,
|
clientVersion: version,
|
||||||
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
phase: "UserSettings.LOADING", // LOADING, DISPLAY
|
||||||
|
email_add_pending: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -83,6 +88,12 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onAvatarPickerClick: function(ev) {
|
||||||
|
if (this.refs.file_label) {
|
||||||
|
this.refs.file_label.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onAvatarSelected: function(ev) {
|
onAvatarSelected: function(ev) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var changeAvatar = this.refs.changeAvatar;
|
var changeAvatar = this.refs.changeAvatar;
|
||||||
|
@ -135,6 +146,12 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onUpgradeClicked: function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "start_upgrade_registration"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onLogoutPromptCancel: function() {
|
onLogoutPromptCancel: function() {
|
||||||
this.logoutModal.closeDialog();
|
this.logoutModal.closeDialog();
|
||||||
},
|
},
|
||||||
|
@ -143,10 +160,81 @@ module.exports = React.createClass({
|
||||||
UserSettingsStore.setEnableNotifications(event.target.checked);
|
UserSettingsStore.setEnableNotifications(event.target.checked);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onAddThreepidClicked: function(value, shouldSubmit) {
|
||||||
|
if (!shouldSubmit) return;
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
|
||||||
|
var email_address = this.refs.add_threepid_input.value;
|
||||||
|
if (!Email.looksValid(email_address)) {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Invalid Email Address",
|
||||||
|
description: "This doesn't appear to be a valid email address",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.add_threepid = new AddThreepid();
|
||||||
|
// we always bind emails when registering, so let's do the
|
||||||
|
// same here.
|
||||||
|
this.add_threepid.addEmailAddress(email_address, true).done(() => {
|
||||||
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
title: "Verification Pending",
|
||||||
|
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
|
||||||
|
button: 'Continue',
|
||||||
|
onFinished: this.onEmailDialogFinished,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Unable to add email address",
|
||||||
|
description: err.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ReactDOM.findDOMNode(this.refs.add_threepid_input).blur();
|
||||||
|
this.setState({email_add_pending: true});
|
||||||
|
},
|
||||||
|
|
||||||
|
onEmailDialogFinished: function(ok) {
|
||||||
|
if (ok) {
|
||||||
|
this.verifyEmailAddress();
|
||||||
|
} else {
|
||||||
|
this.setState({email_add_pending: false});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyEmailAddress: function() {
|
||||||
|
this.add_threepid.checkEmailLinkClicked().done(() => {
|
||||||
|
this.add_threepid = undefined;
|
||||||
|
this.setState({
|
||||||
|
phase: "UserSettings.LOADING",
|
||||||
|
});
|
||||||
|
this._refreshFromServer();
|
||||||
|
this.setState({email_add_pending: false});
|
||||||
|
}, (err) => {
|
||||||
|
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
|
||||||
|
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
var message = "Unable to verify email address. "
|
||||||
|
message += "Please check your email and click on the link it contains. Once this is done, click continue."
|
||||||
|
Modal.createDialog(QuestionDialog, {
|
||||||
|
title: "Verification Pending",
|
||||||
|
description: message,
|
||||||
|
button: 'Continue',
|
||||||
|
onFinished: this.onEmailDialogFinished,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Unable to verify email address",
|
||||||
|
description: err.toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
var self = this;
|
||||||
|
var Loader = sdk.getComponent("elements.Spinner");
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case "UserSettings.LOADING":
|
case "UserSettings.LOADING":
|
||||||
var Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
return (
|
return (
|
||||||
<Loader />
|
<Loader />
|
||||||
);
|
);
|
||||||
|
@ -160,14 +248,76 @@ module.exports = React.createClass({
|
||||||
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
||||||
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||||
|
var Notifications = sdk.getComponent("settings.Notifications");
|
||||||
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
var avatarUrl = (
|
var avatarUrl = (
|
||||||
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var threepidsSection = this.state.threepids.map(function(val, pidIndex) {
|
||||||
|
var id = "email-" + val.address;
|
||||||
|
return (
|
||||||
|
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||||
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
|
<label htmlFor={id}>Email</label>
|
||||||
|
</div>
|
||||||
|
<div className="mx_UserSettings_profileInputCell">
|
||||||
|
<input key={val.address} id={id} value={val.address} disabled />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
var addThreepidSection;
|
||||||
|
if (this.state.email_add_pending) {
|
||||||
|
addThreepidSection = <Loader />;
|
||||||
|
} else {
|
||||||
|
addThreepidSection = (
|
||||||
|
<div className="mx_UserSettings_profileTableRow" key="new">
|
||||||
|
<div className="mx_UserSettings_profileLabelCell">
|
||||||
|
</div>
|
||||||
|
<EditableText
|
||||||
|
ref="add_threepid_input"
|
||||||
|
className="mx_UserSettings_profileInputCell mx_UserSettings_editable"
|
||||||
|
placeholderClassName="mx_RoomSettings_threepidPlaceholder"
|
||||||
|
placeholder={ "Add email address" }
|
||||||
|
blurToCancel={ false }
|
||||||
|
onValueChanged={ this.onAddThreepidClicked } />
|
||||||
|
<div className="mx_RoomSettings_addThreepid">
|
||||||
|
<img src="img/plus.svg" width="14" height="14" alt="Add" onClick={ this.onAddThreepidClicked }/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
threepidsSection.push(addThreepidSection);
|
||||||
|
|
||||||
|
var accountJsx;
|
||||||
|
|
||||||
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
|
accountJsx = (
|
||||||
|
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
|
||||||
|
Create an account
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
accountJsx = (
|
||||||
|
<ChangePassword
|
||||||
|
className="mx_UserSettings_accountTable"
|
||||||
|
rowClassName="mx_UserSettings_profileTableRow"
|
||||||
|
rowLabelClassName="mx_UserSettings_profileLabelCell"
|
||||||
|
rowInputClassName="mx_UserSettings_profileInputCell"
|
||||||
|
buttonClassName="mx_UserSettings_button mx_UserSettings_changePasswordButton"
|
||||||
|
onError={this.onPasswordChangeError}
|
||||||
|
onFinished={this.onPasswordChanged} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserSettings">
|
<div className="mx_UserSettings">
|
||||||
<RoomHeader simpleHeader="Settings" />
|
<RoomHeader simpleHeader="Settings" />
|
||||||
|
|
||||||
|
<GeminiScrollbar className="mx_UserSettings_body" autoshow={true}>
|
||||||
|
|
||||||
<h2>Profile</h2>
|
<h2>Profile</h2>
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
|
@ -180,30 +330,19 @@ module.exports = React.createClass({
|
||||||
<ChangeDisplayName />
|
<ChangeDisplayName />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{threepidsSection}
|
||||||
{this.state.threepids.map(function(val, pidIndex) {
|
|
||||||
var id = "email-" + val.address;
|
|
||||||
return (
|
|
||||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
|
||||||
<div className="mx_UserSettings_profileLabelCell">
|
|
||||||
<label htmlFor={id}>Email</label>
|
|
||||||
</div>
|
|
||||||
<div className="mx_UserSettings_profileInputCell">
|
|
||||||
<input key={val.address} id={id} value={val.address} disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_UserSettings_avatarPicker">
|
<div className="mx_UserSettings_avatarPicker">
|
||||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
<div onClick={ this.onAvatarPickerClick }>
|
||||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||||
|
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||||
|
</div>
|
||||||
<div className="mx_UserSettings_avatarPicker_edit">
|
<div className="mx_UserSettings_avatarPicker_edit">
|
||||||
<label htmlFor="avatarInput">
|
<label htmlFor="avatarInput" ref="file_label">
|
||||||
<img src="img/upload.svg"
|
<img src="img/camera.svg"
|
||||||
alt="Upload avatar" title="Upload avatar"
|
alt="Upload avatar" title="Upload avatar"
|
||||||
width="19" height="24" />
|
width="17" height="15" />
|
||||||
</label>
|
</label>
|
||||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -213,41 +352,18 @@ module.exports = React.createClass({
|
||||||
<h2>Account</h2>
|
<h2>Account</h2>
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<ChangePassword
|
|
||||||
className="mx_UserSettings_accountTable"
|
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||||
rowClassName="mx_UserSettings_profileTableRow"
|
|
||||||
rowLabelClassName="mx_UserSettings_profileLabelCell"
|
|
||||||
rowInputClassName="mx_UserSettings_profileInputCell"
|
|
||||||
buttonClassName="mx_UserSettings_button"
|
|
||||||
onError={this.onPasswordChangeError}
|
|
||||||
onFinished={this.onPasswordChanged} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx_UserSettings_logout">
|
|
||||||
<div className="mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
|
||||||
Log out
|
Log out
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{accountJsx}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Notifications</h2>
|
<h2>Notifications</h2>
|
||||||
|
|
||||||
<div className="mx_UserSettings_section">
|
<div className="mx_UserSettings_section">
|
||||||
<div className="mx_UserSettings_notifTable">
|
<Notifications/>
|
||||||
<div className="mx_UserSettings_notifTableRow">
|
|
||||||
<div className="mx_UserSettings_notifInputCell">
|
|
||||||
<input id="enableNotifications"
|
|
||||||
ref="enableNotifications"
|
|
||||||
type="checkbox"
|
|
||||||
checked={ UserSettingsStore.getEnableNotifications() }
|
|
||||||
onChange={ this.onEnableNotificationsChange } />
|
|
||||||
</div>
|
|
||||||
<div className="mx_UserSettings_notifLabelCell">
|
|
||||||
<label htmlFor="enableNotifications">
|
|
||||||
Enable desktop notifications
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Advanced</h2>
|
<h2>Advanced</h2>
|
||||||
|
@ -260,6 +376,8 @@ module.exports = React.createClass({
|
||||||
Version {this.state.clientVersion}
|
Version {this.state.clientVersion}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</GeminiScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
199
src/components/structures/login/ForgotPassword.js
Normal file
199
src/components/structures/login/ForgotPassword.js
Normal file
|
@ -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 = <Spinner />
|
||||||
|
}
|
||||||
|
else if (this.state.progress === "sent_email") {
|
||||||
|
resetPasswordJsx = (
|
||||||
|
<div>
|
||||||
|
An email has been sent to {this.state.email}. Once you've followed
|
||||||
|
the link it contains, click below.
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||||
|
value="I have verified my email address" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (this.state.progress === "complete") {
|
||||||
|
resetPasswordJsx = (
|
||||||
|
<div>
|
||||||
|
<p>Your password has been reset.</p>
|
||||||
|
<p>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.</p>
|
||||||
|
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||||
|
value="Return to login screen" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
resetPasswordJsx = (
|
||||||
|
<div>
|
||||||
|
To reset your password, enter the email address linked to your account:
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
<form onSubmit={this.onSubmitForm}>
|
||||||
|
<input className="mx_Login_field" ref="user" type="text"
|
||||||
|
value={this.state.email}
|
||||||
|
onChange={this.onInputChanged.bind(this, "email")}
|
||||||
|
placeholder="Email address" autoFocus />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_field" ref="pass" type="password"
|
||||||
|
value={this.state.password}
|
||||||
|
onChange={this.onInputChanged.bind(this, "password")}
|
||||||
|
placeholder="New password" />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_field" ref="pass" type="password"
|
||||||
|
value={this.state.password2}
|
||||||
|
onChange={this.onInputChanged.bind(this, "password2")}
|
||||||
|
placeholder="Confirm your new password" />
|
||||||
|
<br />
|
||||||
|
<input className="mx_Login_submit" type="submit" value="Send Reset Email" />
|
||||||
|
</form>
|
||||||
|
<ServerConfig ref="serverConfig"
|
||||||
|
withToggleButton={true}
|
||||||
|
defaultHsUrl={this.props.homeserverUrl}
|
||||||
|
defaultIsUrl={this.props.identityServerUrl}
|
||||||
|
onHsUrlChanged={this.onHsUrlChanged}
|
||||||
|
onIsUrlChanged={this.onIsUrlChanged}
|
||||||
|
delayTimeMs={0}/>
|
||||||
|
<LoginFooter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_Login">
|
||||||
|
<div className="mx_Login_box">
|
||||||
|
<LoginHeader />
|
||||||
|
{resetPasswordJsx}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login',
|
||||||
homeserverUrl: React.PropTypes.string,
|
homeserverUrl: React.PropTypes.string,
|
||||||
identityServerUrl: React.PropTypes.string,
|
identityServerUrl: React.PropTypes.string,
|
||||||
// login shouldn't know or care how registration is done.
|
// 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() {
|
getDefaultProps: function() {
|
||||||
|
@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login',
|
||||||
switch (step) {
|
switch (step) {
|
||||||
case 'm.login.password':
|
case 'm.login.password':
|
||||||
return (
|
return (
|
||||||
<PasswordLogin onSubmit={this.onPasswordLogin} />
|
<PasswordLogin
|
||||||
|
onSubmit={this.onPasswordLogin}
|
||||||
|
onForgotPasswordClick={this.props.onForgotPasswordClick} />
|
||||||
);
|
);
|
||||||
case 'm.login.cas':
|
case 'm.login.cas':
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
|
||||||
var dis = require('../../../dispatcher');
|
var dis = require('../../../dispatcher');
|
||||||
var Signup = require("../../../Signup");
|
var Signup = require("../../../Signup");
|
||||||
var ServerConfig = require("../../views/login/ServerConfig");
|
var ServerConfig = require("../../views/login/ServerConfig");
|
||||||
|
@ -40,6 +39,9 @@ module.exports = React.createClass({
|
||||||
hsUrl: React.PropTypes.string,
|
hsUrl: React.PropTypes.string,
|
||||||
isUrl: React.PropTypes.string,
|
isUrl: React.PropTypes.string,
|
||||||
email: React.PropTypes.string,
|
email: React.PropTypes.string,
|
||||||
|
username: React.PropTypes.string,
|
||||||
|
guestAccessToken: React.PropTypes.string,
|
||||||
|
disableUsernameChanges: React.PropTypes.bool,
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
onLoginClick: React.PropTypes.func.isRequired
|
onLoginClick: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
@ -63,6 +65,7 @@ module.exports = React.createClass({
|
||||||
this.registerLogic.setSessionId(this.props.sessionId);
|
this.registerLogic.setSessionId(this.props.sessionId);
|
||||||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||||
this.registerLogic.setIdSid(this.props.idSid);
|
this.registerLogic.setIdSid(this.props.idSid);
|
||||||
|
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||||
this.registerLogic.recheckState();
|
this.registerLogic.recheckState();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -156,6 +159,15 @@ module.exports = React.createClass({
|
||||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||||
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
||||||
break;
|
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:
|
default:
|
||||||
console.error("Unknown error code: %s", errCode);
|
console.error("Unknown error code: %s", errCode);
|
||||||
errMsg = "An unknown error occurred.";
|
errMsg = "An unknown error occurred.";
|
||||||
|
@ -186,7 +198,9 @@ module.exports = React.createClass({
|
||||||
registerStep = (
|
registerStep = (
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
showEmail={true}
|
showEmail={true}
|
||||||
|
defaultUsername={this.props.username}
|
||||||
defaultEmail={this.props.email}
|
defaultEmail={this.props.email}
|
||||||
|
disableUsernameChanges={this.props.disableUsernameChanges}
|
||||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||||
onError={this.onFormValidationFailed}
|
onError={this.onFormValidationFailed}
|
||||||
onRegisterClick={this.onFormSubmit} />
|
onRegisterClick={this.onFormSubmit} />
|
||||||
|
|
140
src/components/views/avatars/BaseAvatar.js
Normal file
140
src/components/views/avatars/BaseAvatar.js
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
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, // The name (first initial used as default)
|
||||||
|
idName: React.PropTypes.string, // ID for generating hash colours
|
||||||
|
title: React.PropTypes.string, // onHover title text
|
||||||
|
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||||
|
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number,
|
||||||
|
resizeMethod: React.PropTypes.string,
|
||||||
|
defaultToInitialLetter: React.PropTypes.bool // true to add default url
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
resizeMethod: 'crop',
|
||||||
|
defaultToInitialLetter: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return this._getState(this.props);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
// work out if we need to call setState (if the image URLs array has changed)
|
||||||
|
var newState = this._getState(nextProps);
|
||||||
|
var newImageUrls = newState.imageUrls;
|
||||||
|
var oldImageUrls = this.state.imageUrls;
|
||||||
|
if (newImageUrls.length !== oldImageUrls.length) {
|
||||||
|
this.setState(newState); // detected a new entry
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// check each one to see if they are the same
|
||||||
|
for (var i = 0; i < newImageUrls.length; i++) {
|
||||||
|
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||||
|
this.setState(newState); // detected a diff
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getState: function(props) {
|
||||||
|
// work out the full set of urls to try to load. This is formed like so:
|
||||||
|
// imageUrls: [ props.url, props.urls, default image ]
|
||||||
|
|
||||||
|
var urls = props.urls || [];
|
||||||
|
if (props.url) {
|
||||||
|
urls.unshift(props.url); // put in urls[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultImageUrl = null;
|
||||||
|
if (props.defaultToInitialLetter) {
|
||||||
|
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||||
|
props.idName || props.name
|
||||||
|
);
|
||||||
|
urls.push(defaultImageUrl); // lowest priority
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
imageUrls: urls,
|
||||||
|
defaultImageUrl: defaultImageUrl,
|
||||||
|
urlsIndex: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: function(ev) {
|
||||||
|
var nextIndex = this.state.urlsIndex + 1;
|
||||||
|
if (nextIndex < this.state.imageUrls.length) {
|
||||||
|
// try the next one
|
||||||
|
this.setState({
|
||||||
|
urlsIndex: nextIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getInitialLetter: function() {
|
||||||
|
var name = this.props.name;
|
||||||
|
var initial = name[0];
|
||||||
|
if ((initial === '@' || initial === '#') && name[1]) {
|
||||||
|
initial = name[1];
|
||||||
|
}
|
||||||
|
return initial.toUpperCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var name = this.props.name;
|
||||||
|
|
||||||
|
var imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||||
|
|
||||||
|
if (imageUrl === this.state.defaultImageUrl) {
|
||||||
|
var initialLetter = this._getInitialLetter();
|
||||||
|
return (
|
||||||
|
<span className="mx_BaseAvatar" {...this.props}>
|
||||||
|
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||||
|
style={{ fontSize: (this.props.width * 0.65) + "px",
|
||||||
|
width: this.props.width + "px",
|
||||||
|
lineHeight: this.props.height + "px" }}>
|
||||||
|
{ initialLetter }
|
||||||
|
</span>
|
||||||
|
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||||
|
title={this.props.title} onError={this.onError}
|
||||||
|
width={this.props.width} height={this.props.height} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<img className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl}
|
||||||
|
onError={this.onError}
|
||||||
|
width={this.props.width} height={this.props.height}
|
||||||
|
title={this.props.title}
|
||||||
|
{...this.props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var Avatar = require('../../../Avatar');
|
var Avatar = require('../../../Avatar');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var sdk = require("../../../index");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MemberAvatar',
|
displayName: 'MemberAvatar',
|
||||||
|
@ -27,7 +27,7 @@ module.exports = React.createClass({
|
||||||
member: React.PropTypes.object.isRequired,
|
member: React.PropTypes.object.isRequired,
|
||||||
width: React.PropTypes.number,
|
width: React.PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: React.PropTypes.number,
|
||||||
resizeMethod: React.PropTypes.string,
|
resizeMethod: React.PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -38,75 +38,30 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
imageUrl: this.defaultAvatarUrl(this.props.member)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_computeUrl: function() {
|
|
||||||
return Avatar.avatarUrlForMember(this.props.member,
|
|
||||||
this.props.width,
|
|
||||||
this.props.height,
|
|
||||||
this.props.resizeMethod);
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshUrl: function() {
|
|
||||||
var newUrl = this._computeUrl();
|
|
||||||
if (newUrl != this.currentUrl) {
|
|
||||||
this.currentUrl = newUrl;
|
|
||||||
this.setState({imageUrl: newUrl});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return this._getState(this.props);
|
||||||
imageUrl: this._computeUrl()
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.setState(this._getState(nextProps));
|
||||||
|
},
|
||||||
|
|
||||||
///////////////
|
_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() {
|
render: function() {
|
||||||
// XXX: recalculates default avatar url constantly
|
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
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();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="mx_MemberAvatar" {...this.props}>
|
|
||||||
<span className="mx_MemberAvatar_initial" aria-hidden="true"
|
|
||||||
style={{ fontSize: (this.props.width * 0.65) + "px",
|
|
||||||
width: this.props.width + "px",
|
|
||||||
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
|
||||||
<img className="mx_MemberAvatar_image" src={this.state.imageUrl} title={this.props.member.name}
|
|
||||||
onError={this.onError} width={this.props.width} height={this.props.height} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
|
<BaseAvatar {...this.props} name={this.state.name} title={this.state.title}
|
||||||
onError={this.onError}
|
idName={this.props.member.userId} url={this.state.imageUrl} />
|
||||||
width={this.props.width} height={this.props.height}
|
|
||||||
title={this.props.member.name}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,10 +16,18 @@ limitations under the License.
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
var Avatar = require('../../../Avatar');
|
var Avatar = require('../../../Avatar');
|
||||||
|
var sdk = require("../../../index");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomAvatar',
|
displayName: 'RoomAvatar',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
room: React.PropTypes.object.isRequired,
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number,
|
||||||
|
resizeMethod: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
width: 36,
|
width: 36,
|
||||||
|
@ -29,84 +37,54 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
this._update();
|
|
||||||
return {
|
return {
|
||||||
imageUrl: this._nextUrl()
|
urls: this.getImageUrls(this.props)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
this.refreshImageUrl();
|
this.setState({
|
||||||
|
urls: this.getImageUrls(newProps)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshImageUrl: function(nextProps) {
|
getImageUrls: function(props) {
|
||||||
// If the list has changed, we start from scratch and re-check, but
|
return [
|
||||||
// don't do so unless the list has changed or we'd re-try fetching
|
this.getRoomAvatarUrl(props), // highest priority
|
||||||
// images each time we re-rendered
|
this.getOneToOneAvatar(props),
|
||||||
var newList = this.getUrlList();
|
this.getFallbackAvatar(props) // lowest priority
|
||||||
var differs = false;
|
].filter(function(url) {
|
||||||
for (var i = 0; i < newList.length && i < this.urlList.length; ++i) {
|
return url != null;
|
||||||
if (this.urlList[i] != newList[i]) differs = true;
|
});
|
||||||
}
|
|
||||||
if (this.urlList.length != newList.length) differs = true;
|
|
||||||
|
|
||||||
if (differs) {
|
|
||||||
this._update();
|
|
||||||
this.setState({
|
|
||||||
imageUrl: this._nextUrl()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_update: function() {
|
getRoomAvatarUrl: function(props) {
|
||||||
this.urlList = this.getUrlList();
|
return props.room.getAvatarUrl(
|
||||||
this.urlListIndex = -1;
|
|
||||||
},
|
|
||||||
|
|
||||||
_nextUrl: function() {
|
|
||||||
do {
|
|
||||||
++this.urlListIndex;
|
|
||||||
} while (
|
|
||||||
this.urlList[this.urlListIndex] === null &&
|
|
||||||
this.urlListIndex < this.urlList.length
|
|
||||||
);
|
|
||||||
if (this.urlListIndex < this.urlList.length) {
|
|
||||||
return this.urlList[this.urlListIndex];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// provided to the view class for convenience
|
|
||||||
roomAvatarUrl: function() {
|
|
||||||
var url = this.props.room.getAvatarUrl(
|
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
this.props.width, this.props.height, this.props.resizeMethod,
|
props.width, props.height, props.resizeMethod,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
return url;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// provided to the view class for convenience
|
getOneToOneAvatar: function(props) {
|
||||||
getOneToOneAvatar: function() {
|
var userIds = Object.keys(props.room.currentState.members);
|
||||||
var userIds = Object.keys(this.props.room.currentState.members);
|
|
||||||
|
|
||||||
if (userIds.length == 2) {
|
if (userIds.length == 2) {
|
||||||
var theOtherGuy = null;
|
var theOtherGuy = null;
|
||||||
if (this.props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
if (props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
theOtherGuy = this.props.room.currentState.members[userIds[1]];
|
theOtherGuy = props.room.currentState.members[userIds[1]];
|
||||||
} else {
|
} else {
|
||||||
theOtherGuy = this.props.room.currentState.members[userIds[0]];
|
theOtherGuy = props.room.currentState.members[userIds[0]];
|
||||||
}
|
}
|
||||||
return theOtherGuy.getAvatarUrl(
|
return theOtherGuy.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
this.props.width, this.props.height, this.props.resizeMethod,
|
props.width, props.height, props.resizeMethod,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else if (userIds.length == 1) {
|
} else if (userIds.length == 1) {
|
||||||
return this.props.room.currentState.members[userIds[0]].getAvatarUrl(
|
return props.room.currentState.members[userIds[0]].getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
this.props.width, this.props.height, this.props.resizeMethod,
|
props.width, props.height, props.resizeMethod,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,58 +92,15 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFallbackAvatar: function(props) {
|
||||||
onError: function(ev) {
|
return Avatar.defaultAvatarUrlForString(props.room.roomId);
|
||||||
this.setState({
|
|
||||||
imageUrl: this._nextUrl()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////
|
|
||||||
|
|
||||||
|
|
||||||
getUrlList: function() {
|
|
||||||
return [
|
|
||||||
this.roomAvatarUrl(),
|
|
||||||
this.getOneToOneAvatar(),
|
|
||||||
this.getFallbackAvatar()
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
getFallbackAvatar: function() {
|
|
||||||
return Avatar.defaultAvatarUrlForString(this.props.room.roomId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var style = {
|
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||||
width: this.props.width,
|
return (
|
||||||
height: this.props.height,
|
<BaseAvatar {...this.props} name={this.props.room.name}
|
||||||
};
|
idName={this.props.room.roomId} urls={this.state.urls} />
|
||||||
|
);
|
||||||
// XXX: recalculates fallback avatar constantly
|
|
||||||
if (this.state.imageUrl === this.getFallbackAvatar()) {
|
|
||||||
var initial;
|
|
||||||
if (this.props.room.name[0])
|
|
||||||
initial = this.props.room.name[0].toUpperCase();
|
|
||||||
if ((initial === '@' || initial === '#') && this.props.room.name[1])
|
|
||||||
initial = this.props.room.name[1].toUpperCase();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<span className="mx_RoomAvatar_initial" aria-hidden="true"
|
|
||||||
style={{ fontSize: (this.props.width * 0.65) + "px",
|
|
||||||
width: this.props.width + "px",
|
|
||||||
lineHeight: this.props.height + "px" }}>{ initial }</span>
|
|
||||||
<img className="mx_RoomAvatar" src={this.state.imageUrl}
|
|
||||||
onError={this.onError} style={style} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return <img className="mx_RoomAvatar" src={this.state.imageUrl}
|
|
||||||
onError={this.onError} style={style} />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,7 +48,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ErrorDialog">
|
<div className="mx_ErrorDialog">
|
||||||
<div className="mx_ErrorDialogTitle">
|
<div className="mx_Dialog_title">
|
||||||
{this.props.title}
|
{this.props.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_QuestionDialog">
|
<div className="mx_QuestionDialog">
|
||||||
<div className="mx_QuestionDialogTitle">
|
<div className="mx_Dialog_title">
|
||||||
{this.props.title}
|
{this.props.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
|
|
94
src/components/views/dialogs/TextInputDialog.js
Normal file
94
src/components/views/dialogs/TextInputDialog.js
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
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 React = require("react");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'TextInputDialog',
|
||||||
|
propTypes: {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
description: React.PropTypes.string,
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
button: React.PropTypes.string,
|
||||||
|
focus: React.PropTypes.bool,
|
||||||
|
onFinished: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
value: "",
|
||||||
|
description: "",
|
||||||
|
button: "OK",
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onOk: function() {
|
||||||
|
this.props.onFinished(true, this.refs.textinput.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel: function() {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown: function(e) {
|
||||||
|
if (e.keyCode === 27) { // escape
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
}
|
||||||
|
else if (e.keyCode === 13) { // enter
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onFinished(true, this.refs.textinput.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<div className="mx_TextInputDialog">
|
||||||
|
<div className="mx_Dialog_title">
|
||||||
|
{this.props.title}
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_content">
|
||||||
|
<div className="mx_TextInputDialog_label">
|
||||||
|
<label htmlFor="textinput"> {this.props.description} </label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" onKeyDown={this.onKeyDown}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this.onOk}>
|
||||||
|
{this.props.button}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={this.onCancel}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -18,13 +18,22 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
|
||||||
|
const KEY_TAB = 9;
|
||||||
|
const KEY_SHIFT = 16;
|
||||||
|
const KEY_WINDOWS = 91;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'EditableText',
|
displayName: 'EditableText',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onValueChanged: React.PropTypes.func,
|
onValueChanged: React.PropTypes.func,
|
||||||
initialValue: React.PropTypes.string,
|
initialValue: React.PropTypes.string,
|
||||||
label: React.PropTypes.string,
|
label: React.PropTypes.string,
|
||||||
placeHolder: React.PropTypes.string,
|
placeholder: React.PropTypes.string,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
labelClassName: React.PropTypes.string,
|
||||||
|
placeholderClassName: React.PropTypes.string,
|
||||||
|
blurToCancel: React.PropTypes.bool,
|
||||||
|
editable: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
Phases: {
|
Phases: {
|
||||||
|
@ -36,38 +45,62 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
onValueChanged: function() {},
|
onValueChanged: function() {},
|
||||||
initialValue: '',
|
initialValue: '',
|
||||||
label: 'Click to set',
|
label: '',
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
|
editable: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
value: this.props.initialValue,
|
|
||||||
phase: this.Phases.Display,
|
phase: this.Phases.Display,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
componentWillReceiveProps: function(nextProps) {
|
||||||
this.setState({
|
if (nextProps.initialValue !== this.props.initialValue) {
|
||||||
value: nextProps.initialValue
|
this.value = nextProps.initialValue;
|
||||||
});
|
if (this.refs.editable_div) {
|
||||||
|
this.showPlaceholder(!this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
// we track value as an JS object field rather than in React state
|
||||||
|
// as React doesn't play nice with contentEditable.
|
||||||
|
this.value = '';
|
||||||
|
this.placeholder = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.value = this.props.initialValue;
|
||||||
|
if (this.refs.editable_div) {
|
||||||
|
this.showPlaceholder(!this.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showPlaceholder: function(show) {
|
||||||
|
if (show) {
|
||||||
|
this.refs.editable_div.textContent = this.props.placeholder;
|
||||||
|
this.refs.editable_div.setAttribute("class", this.props.className + " " + this.props.placeholderClassName);
|
||||||
|
this.placeholder = true;
|
||||||
|
this.value = '';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.refs.editable_div.textContent = this.value;
|
||||||
|
this.refs.editable_div.setAttribute("class", this.props.className);
|
||||||
|
this.placeholder = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getValue: function() {
|
getValue: function() {
|
||||||
return this.state.value;
|
return this.value;
|
||||||
},
|
},
|
||||||
|
|
||||||
setValue: function(val, shouldSubmit, suppressListener) {
|
setValue: function(value) {
|
||||||
var self = this;
|
this.value = value;
|
||||||
this.setState({
|
this.showPlaceholder(!this.value);
|
||||||
value: val,
|
|
||||||
phase: this.Phases.Display,
|
|
||||||
}, function() {
|
|
||||||
if (!suppressListener) {
|
|
||||||
self.onValueChanged(shouldSubmit);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
edit: function() {
|
edit: function() {
|
||||||
|
@ -80,65 +113,106 @@ module.exports = React.createClass({
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Display,
|
phase: this.Phases.Display,
|
||||||
});
|
});
|
||||||
|
this.value = this.props.initialValue;
|
||||||
|
this.showPlaceholder(!this.value);
|
||||||
this.onValueChanged(false);
|
this.onValueChanged(false);
|
||||||
},
|
},
|
||||||
|
|
||||||
onValueChanged: function(shouldSubmit) {
|
onValueChanged: function(shouldSubmit) {
|
||||||
this.props.onValueChanged(this.state.value, shouldSubmit);
|
this.props.onValueChanged(this.value, shouldSubmit);
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeyDown: function(ev) {
|
||||||
|
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
|
|
||||||
|
if (this.placeholder) {
|
||||||
|
this.showPlaceholder(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.key == "Enter") {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("keyDown: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: function(ev) {
|
onKeyUp: function(ev) {
|
||||||
|
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
|
|
||||||
|
if (!ev.target.textContent) {
|
||||||
|
this.showPlaceholder(true);
|
||||||
|
}
|
||||||
|
else if (!this.placeholder) {
|
||||||
|
this.value = ev.target.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
if (ev.key == "Enter") {
|
if (ev.key == "Enter") {
|
||||||
this.onFinish(ev);
|
this.onFinish(ev);
|
||||||
} else if (ev.key == "Escape") {
|
} else if (ev.key == "Escape") {
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log("keyUp: textContent=" + ev.target.textContent + ", value=" + this.value + ", placeholder=" + this.placeholder);
|
||||||
},
|
},
|
||||||
|
|
||||||
onClickDiv: function() {
|
onClickDiv: function(ev) {
|
||||||
|
if (!this.props.editable) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.Phases.Edit,
|
phase: this.Phases.Edit,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onFocus: function(ev) {
|
onFocus: function(ev) {
|
||||||
ev.target.setSelectionRange(0, ev.target.value.length);
|
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||||
},
|
|
||||||
|
|
||||||
onFinish: function(ev) {
|
var node = ev.target.childNodes[0];
|
||||||
if (ev.target.value) {
|
if (node) {
|
||||||
this.setValue(ev.target.value, ev.key === "Enter");
|
var range = document.createRange();
|
||||||
} else {
|
range.setStart(node, 0);
|
||||||
this.cancelEdit();
|
range.setEnd(node, node.length);
|
||||||
|
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlur: function() {
|
onFinish: function(ev) {
|
||||||
this.cancelEdit();
|
var self = this;
|
||||||
|
var submit = (ev.key === "Enter");
|
||||||
|
this.setState({
|
||||||
|
phase: this.Phases.Display,
|
||||||
|
}, function() {
|
||||||
|
self.onValueChanged(submit);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onBlur: function(ev) {
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
|
||||||
|
if (this.props.blurToCancel)
|
||||||
|
this.cancelEdit();
|
||||||
|
else
|
||||||
|
this.onFinish(ev);
|
||||||
|
|
||||||
|
this.showPlaceholder(!this.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var editable_el;
|
var editable_el;
|
||||||
|
|
||||||
if (this.state.phase == this.Phases.Display) {
|
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||||
if (this.state.value) {
|
// show the label
|
||||||
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
|
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||||
} else {
|
} else {
|
||||||
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>;
|
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||||
}
|
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
||||||
} else if (this.state.phase == this.Phases.Edit) {
|
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||||
editable_el = (
|
|
||||||
<div>
|
|
||||||
<input type="text" defaultValue={this.state.value}
|
|
||||||
onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur} placeholder={this.props.placeHolder} autoFocus/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return editable_el;
|
||||||
<div className="mx_EditableText">
|
|
||||||
{editable_el}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
108
src/components/views/elements/PowerSelector.js
Normal file
108
src/components/views/elements/PowerSelector.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
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 roles = {
|
||||||
|
0: 'User',
|
||||||
|
50: 'Moderator',
|
||||||
|
100: 'Admin',
|
||||||
|
};
|
||||||
|
|
||||||
|
var reverseRoles = {};
|
||||||
|
Object.keys(roles).forEach(function(key) {
|
||||||
|
reverseRoles[roles[key]] = key;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'PowerSelector',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
value: React.PropTypes.number.isRequired,
|
||||||
|
disabled: React.PropTypes.bool,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
custom: (roles[this.props.value] === undefined),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
onSelectChange: function(event) {
|
||||||
|
this.state.custom = (event.target.value === "Custom");
|
||||||
|
this.props.onChange(this.getValue());
|
||||||
|
},
|
||||||
|
|
||||||
|
onCustomBlur: function(event) {
|
||||||
|
this.props.onChange(this.getValue());
|
||||||
|
},
|
||||||
|
|
||||||
|
onCustomKeyDown: function(event) {
|
||||||
|
if (event.key == "Enter") {
|
||||||
|
this.props.onChange(this.getValue());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getValue: function() {
|
||||||
|
var value;
|
||||||
|
if (this.refs.select) {
|
||||||
|
value = reverseRoles[ this.refs.select.value ];
|
||||||
|
if (this.refs.custom) {
|
||||||
|
if (value === undefined) value = parseInt( this.refs.custom.value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var customPicker;
|
||||||
|
if (this.state.custom) {
|
||||||
|
var input;
|
||||||
|
if (this.props.disabled) {
|
||||||
|
input = <span>{ this.props.value }</span>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input = <input ref="custom" type="text" size="3" defaultValue={ this.props.value } onBlur={ this.onCustomBlur } onKeyDown={ this.onCustomKeyDown }/>
|
||||||
|
}
|
||||||
|
customPicker = <span> of { input }</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectValue = roles[this.props.value] || "Custom";
|
||||||
|
var select;
|
||||||
|
if (this.props.disabled) {
|
||||||
|
select = <span>{ selectValue }</span>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
select =
|
||||||
|
<select ref="select" defaultValue={ selectValue } onChange={ this.onSelectChange }>
|
||||||
|
<option value="User">User (0)</option>
|
||||||
|
<option value="Moderator">Moderator (50)</option>
|
||||||
|
<option value="Admin">Admin (100)</option>
|
||||||
|
<option value="Custom">Custom level</option>
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="mx_PowerSelector">
|
||||||
|
{ select }
|
||||||
|
{ customPicker }
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
69
src/components/views/elements/TintableSvg.js
Normal file
69
src/components/views/elements/TintableSvg.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var ReactDOM = require("react-dom");
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
var Tinter = require("../../../Tinter");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'TintableSvg',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
src: React.PropTypes.string.isRequired,
|
||||||
|
width: React.PropTypes.string.isRequired,
|
||||||
|
height: React.PropTypes.string.isRequired,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.fixups = [];
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
// we can't use onLoad on object due to https://github.com/facebook/react/pull/5781
|
||||||
|
// so handle it with pure DOM instead
|
||||||
|
ReactDOM.findDOMNode(this).addEventListener('load', this.onLoad);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
ReactDOM.findDOMNode(this).removeEventListener('load', this.onLoad);
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
|
},
|
||||||
|
|
||||||
|
onAction: function(payload) {
|
||||||
|
if (payload.action !== 'tint_update') return;
|
||||||
|
Tinter.applySvgFixups(this.fixups);
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad: function(event) {
|
||||||
|
this.fixups = Tinter.calcSvgFixups([event.target]);
|
||||||
|
Tinter.applySvgFixups(this.fixups);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
return (
|
||||||
|
<object className={ "mx_TintableSvg " + this.props.className }
|
||||||
|
type="image/svg+xml"
|
||||||
|
data={ this.props.src }
|
||||||
|
width={ this.props.width }
|
||||||
|
height={ this.props.height }/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -22,7 +22,7 @@ module.exports = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_ErrorDialog">
|
<div className="mx_ErrorDialog">
|
||||||
<div className="mx_ErrorDialogTitle">
|
<div className="mx_Dialog_title">
|
||||||
Custom Server Options
|
Custom Server Options
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
|
|
|
@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
|
||||||
*/
|
*/
|
||||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
onSubmit: React.PropTypes.func.isRequired // fn(username, password)
|
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||||
|
onForgotPasswordClick: React.PropTypes.func // fn()
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
var forgotPasswordJsx;
|
||||||
|
|
||||||
|
if (this.props.onForgotPasswordClick) {
|
||||||
|
forgotPasswordJsx = (
|
||||||
|
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
|
||||||
|
Forgot your password?
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
|
@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
value={this.state.password} onChange={this.onPasswordChanged}
|
value={this.state.password} onChange={this.onPasswordChanged}
|
||||||
placeholder="Password" />
|
placeholder="Password" />
|
||||||
<br />
|
<br />
|
||||||
|
{forgotPasswordJsx}
|
||||||
<input className="mx_Login_submit" type="submit" value="Log in" />
|
<input className="mx_Login_submit" type="submit" value="Log in" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,7 +17,15 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
|
var Velocity = require('velocity-animate');
|
||||||
|
require('velocity-ui-pack');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
var Email = require('../../../email');
|
||||||
|
|
||||||
|
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.
|
* A pure UI component which displays a registration form.
|
||||||
|
@ -30,6 +38,7 @@ module.exports = React.createClass({
|
||||||
defaultUsername: React.PropTypes.string,
|
defaultUsername: React.PropTypes.string,
|
||||||
showEmail: React.PropTypes.bool,
|
showEmail: React.PropTypes.bool,
|
||||||
minPasswordLength: React.PropTypes.number,
|
minPasswordLength: React.PropTypes.number,
|
||||||
|
disableUsernameChanges: React.PropTypes.bool,
|
||||||
onError: React.PropTypes.func,
|
onError: React.PropTypes.func,
|
||||||
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
||||||
},
|
},
|
||||||
|
@ -49,52 +58,151 @@ module.exports = React.createClass({
|
||||||
email: this.props.defaultEmail,
|
email: this.props.defaultEmail,
|
||||||
username: this.props.defaultUsername,
|
username: this.props.defaultUsername,
|
||||||
password: null,
|
password: null,
|
||||||
passwordConfirm: null
|
passwordConfirm: null,
|
||||||
|
fieldValid: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit: function(ev) {
|
onSubmit: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
var pwd1 = this.refs.password.value.trim();
|
// validate everything, in reverse order so
|
||||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
// the error that ends up being displayed
|
||||||
|
// is the one from the first invalid field.
|
||||||
|
// It's not super ideal that this just calls
|
||||||
|
// onError once for each invalid field.
|
||||||
|
this.validateField(FIELD_PASSWORD_CONFIRM);
|
||||||
|
this.validateField(FIELD_PASSWORD);
|
||||||
|
this.validateField(FIELD_USERNAME);
|
||||||
|
this.validateField(FIELD_EMAIL);
|
||||||
|
|
||||||
var errCode;
|
if (this.allFieldsValid()) {
|
||||||
if (!pwd1 || !pwd2) {
|
var promise = this.props.onRegisterClick({
|
||||||
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
|
username: this.refs.username.value.trim(),
|
||||||
}
|
password: this.refs.password.value.trim(),
|
||||||
else if (pwd1 !== pwd2) {
|
email: this.refs.email.value.trim()
|
||||||
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,
|
|
||||||
email: this.refs.email.value.trim()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (promise) {
|
|
||||||
ev.target.disabled = true;
|
|
||||||
promise.finally(function() {
|
|
||||||
ev.target.disabled = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (promise) {
|
||||||
|
ev.target.disabled = true;
|
||||||
|
promise.finally(function() {
|
||||||
|
ev.target.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if all fields were valid last time
|
||||||
|
* they were validated.
|
||||||
|
*/
|
||||||
|
allFieldsValid: function() {
|
||||||
|
var keys = Object.keys(this.state.fieldValid);
|
||||||
|
for (var i = 0; i < keys.length; ++i) {
|
||||||
|
if (this.state.fieldValid[keys[i]] == false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
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 == '' || Email.looksValid(this.refs.email.value),
|
||||||
|
"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_LENGTH"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.markFieldValid(field_id, true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FIELD_PASSWORD_CONFIRM:
|
||||||
|
this.markFieldValid(
|
||||||
|
field_id, pwd1 == pwd2,
|
||||||
|
"RegistrationForm.ERR_PASSWORD_MISMATCH"
|
||||||
|
);
|
||||||
|
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() {
|
render: function() {
|
||||||
|
var self = this;
|
||||||
var emailSection, registerButton;
|
var emailSection, registerButton;
|
||||||
if (this.props.showEmail) {
|
if (this.props.showEmail) {
|
||||||
emailSection = (
|
emailSection = (
|
||||||
<input className="mx_Login_field" type="text" ref="email"
|
<input className="mx_Login_field" type="text" ref="email"
|
||||||
autoFocus={true} placeholder="Email address"
|
autoFocus={true} placeholder="Email address"
|
||||||
defaultValue={this.state.email} />
|
defaultValue={this.state.email}
|
||||||
|
style={this._styleField(FIELD_EMAIL)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_EMAIL)}} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.props.onRegisterClick) {
|
if (this.props.onRegisterClick) {
|
||||||
|
@ -109,13 +217,20 @@ module.exports = React.createClass({
|
||||||
{emailSection}
|
{emailSection}
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" type="text" ref="username"
|
<input className="mx_Login_field" type="text" ref="username"
|
||||||
placeholder="User name" defaultValue={this.state.username} />
|
placeholder="User name" defaultValue={this.state.username}
|
||||||
|
style={this._styleField(FIELD_USERNAME)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_USERNAME)}}
|
||||||
|
disabled={this.props.disableUsernameChanges} />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" type="password" ref="password"
|
<input className="mx_Login_field" type="password" ref="password"
|
||||||
|
style={this._styleField(FIELD_PASSWORD)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_PASSWORD)}}
|
||||||
placeholder="Password" defaultValue={this.state.password} />
|
placeholder="Password" defaultValue={this.state.password} />
|
||||||
<br />
|
<br />
|
||||||
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
|
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
||||||
defaultValue={this.state.passwordConfirm} />
|
defaultValue={this.state.passwordConfirm} />
|
||||||
<br />
|
<br />
|
||||||
{registerButton}
|
{registerButton}
|
||||||
|
|
|
@ -19,6 +19,8 @@ limitations under the License.
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var filesize = require('filesize');
|
var filesize = require('filesize');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MFileBody',
|
displayName: 'MFileBody',
|
||||||
|
@ -52,12 +54,14 @@ module.exports = React.createClass({
|
||||||
var httpUrl = cli.mxcUrlToHttp(content.url);
|
var httpUrl = cli.mxcUrlToHttp(content.url);
|
||||||
var text = this.presentableTextForFile(content);
|
var text = this.presentableTextForFile(content);
|
||||||
|
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
if (httpUrl) {
|
if (httpUrl) {
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
<img src="img/download.png" width="10" height="12"/>
|
<TintableSvg src="img/download.svg" width="12" height="14"/>
|
||||||
Download {text}
|
Download {text}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,6 +22,7 @@ var filesize = require('filesize');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
var Modal = require('../../../Modal');
|
var Modal = require('../../../Modal');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
var dis = require("../../../dispatcher");
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MImageBody',
|
displayName: 'MImageBody',
|
||||||
|
@ -97,6 +98,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
var content = this.props.mxEvent.getContent();
|
var content = this.props.mxEvent.getContent();
|
||||||
var cli = MatrixClientPeg.get();
|
var cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ module.exports = React.createClass({
|
||||||
</a>
|
</a>
|
||||||
<div className="mx_MImageBody_download">
|
<div className="mx_MImageBody_download">
|
||||||
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">
|
||||||
<img src="img/download.png" width="10" height="12"/>
|
<TintableSvg src="img/download.svg" width="12" height="14"/>
|
||||||
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
|
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,6 +36,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function() {
|
componentDidUpdate: function() {
|
||||||
|
// XXX: why don't we linkify here?
|
||||||
|
// XXX: why do we bother doing this on update at all, given events are immutable?
|
||||||
|
|
||||||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
|
||||||
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
|
||||||
},
|
},
|
||||||
|
|
139
src/components/views/rooms/EntityTile.js
Normal file
139
src/components/views/rooms/EntityTile.js
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
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');
|
||||||
|
|
||||||
|
|
||||||
|
var PRESENCE_CLASS = {
|
||||||
|
"offline": "mx_EntityTile_offline",
|
||||||
|
"online": "mx_EntityTile_online",
|
||||||
|
"unavailable": "mx_EntityTile_unavailable"
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'EntityTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
name: React.PropTypes.string,
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
avatarJsx: React.PropTypes.any, // <BaseAvatar />
|
||||||
|
presenceState: React.PropTypes.string,
|
||||||
|
presenceActiveAgo: React.PropTypes.number,
|
||||||
|
showInviteButton: React.PropTypes.bool,
|
||||||
|
shouldComponentUpdate: React.PropTypes.func,
|
||||||
|
onClick: React.PropTypes.func
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
shouldComponentUpdate: function(nextProps, nextState) { return false; },
|
||||||
|
onClick: function() {},
|
||||||
|
presenceState: "offline",
|
||||||
|
presenceActiveAgo: -1,
|
||||||
|
showInviteButton: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
hover: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
|
if (this.state.hover !== nextState.hover) return true;
|
||||||
|
return this.props.shouldComponentUpdate(nextProps, nextState);
|
||||||
|
},
|
||||||
|
|
||||||
|
mouseEnter: function(e) {
|
||||||
|
this.setState({ 'hover': true });
|
||||||
|
},
|
||||||
|
|
||||||
|
mouseLeave: function(e) {
|
||||||
|
this.setState({ 'hover': false });
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var presenceClass = PRESENCE_CLASS[this.props.presenceState] || "mx_EntityTile_offline";
|
||||||
|
var mainClassName = "mx_EntityTile ";
|
||||||
|
mainClassName += presenceClass;
|
||||||
|
if (this.state.hover) {
|
||||||
|
mainClassName += " mx_EntityTile_hover";
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameEl;
|
||||||
|
if (this.state.hover) {
|
||||||
|
var PresenceLabel = sdk.getComponent("rooms.PresenceLabel");
|
||||||
|
nameEl = (
|
||||||
|
<div className="mx_EntityTile_details">
|
||||||
|
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
||||||
|
<div className="mx_EntityTile_name_hover">{ this.props.name }</div>
|
||||||
|
<PresenceLabel activeAgo={this.props.presenceActiveAgo}
|
||||||
|
presenceState={this.props.presenceState} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nameEl = (
|
||||||
|
<div className="mx_EntityTile_name">
|
||||||
|
{ this.props.name }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var inviteButton;
|
||||||
|
if (this.props.showInviteButton) {
|
||||||
|
inviteButton = (
|
||||||
|
<div className="mx_EntityTile_invite">
|
||||||
|
<img src="img/plus.svg" width="16" height="16" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var power;
|
||||||
|
var powerLevel = this.props.powerLevel;
|
||||||
|
if (powerLevel >= 50 && powerLevel < 99) {
|
||||||
|
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt="Mod"/>;
|
||||||
|
}
|
||||||
|
if (powerLevel >= 99) {
|
||||||
|
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt="Admin"/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
|
||||||
|
var av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mainClassName} title={ this.props.title }
|
||||||
|
onClick={ this.props.onClick } onMouseEnter={ this.mouseEnter }
|
||||||
|
onMouseLeave={ this.mouseLeave }>
|
||||||
|
<div className="mx_EntityTile_avatar">
|
||||||
|
{ av }
|
||||||
|
{ power }
|
||||||
|
</div>
|
||||||
|
{ nameEl }
|
||||||
|
{ inviteButton }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -58,15 +58,16 @@ module.exports = React.createClass({
|
||||||
var roomId = this.props.member.roomId;
|
var roomId = this.props.member.roomId;
|
||||||
var target = this.props.member.userId;
|
var target = this.props.member.userId;
|
||||||
MatrixClientPeg.get().kick(roomId, target).done(function() {
|
MatrixClientPeg.get().kick(roomId, target).done(function() {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Kick success");
|
console.log("Kick success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Kick error",
|
title: "Kick error",
|
||||||
description: err.message
|
description: err.message
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -74,16 +75,18 @@ module.exports = React.createClass({
|
||||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
var roomId = this.props.member.roomId;
|
var roomId = this.props.member.roomId;
|
||||||
var target = this.props.member.userId;
|
var target = this.props.member.userId;
|
||||||
MatrixClientPeg.get().ban(roomId, target).done(function() {
|
MatrixClientPeg.get().ban(roomId, target).done(
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
function() {
|
||||||
// get out of sync if we force setState here!
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
console.log("Ban success");
|
// get out of sync if we force setState here!
|
||||||
}, function(err) {
|
console.log("Ban success");
|
||||||
Modal.createDialog(ErrorDialog, {
|
}, function(err) {
|
||||||
title: "Ban error",
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: err.message
|
title: "Ban error",
|
||||||
});
|
description: err.message
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -118,16 +121,17 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done(
|
MatrixClientPeg.get().setPowerLevel(roomId, target, level, powerLevelEvent).done(
|
||||||
function() {
|
function() {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Mute toggle success");
|
console.log("Mute toggle success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Mute error",
|
title: "Mute error",
|
||||||
description: err.message
|
description: err.message
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -154,22 +158,55 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
var defaultLevel = powerLevelEvent.getContent().users_default;
|
var defaultLevel = powerLevelEvent.getContent().users_default;
|
||||||
var modLevel = me.powerLevel - 1;
|
var modLevel = me.powerLevel - 1;
|
||||||
|
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
|
||||||
// toggle the level
|
// toggle the level
|
||||||
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
|
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
|
||||||
MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
|
MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
|
||||||
function() {
|
function() {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
console.log("Mod toggle success");
|
console.log("Mod toggle success");
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: "Mod error",
|
title: "Mod error",
|
||||||
description: err.message
|
description: err.message
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPowerChange: function(powerLevel) {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
var roomId = this.props.member.roomId;
|
||||||
|
var target = this.props.member.userId;
|
||||||
|
var room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
this.props.onFinished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var powerLevelEvent = room.currentState.getStateEvents(
|
||||||
|
"m.room.power_levels", ""
|
||||||
|
);
|
||||||
|
if (!powerLevelEvent) {
|
||||||
|
this.props.onFinished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MatrixClientPeg.get().setPowerLevel(roomId, target, powerLevel, powerLevelEvent).done(
|
||||||
|
function() {
|
||||||
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
|
// get out of sync if we force setState here!
|
||||||
|
console.log("Power change success");
|
||||||
|
}, function(err) {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Failure to change power level",
|
||||||
|
description: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.props.onFinished();
|
||||||
|
},
|
||||||
|
|
||||||
onChatClick: function() {
|
onChatClick: function() {
|
||||||
// check if there are any existing rooms with just us and them (1:1)
|
// check if there are any existing rooms with just us and them (1:1)
|
||||||
// If so, just view that room. If not, create a private room with them.
|
// If so, just view that room. If not, create a private room with them.
|
||||||
|
@ -209,20 +246,22 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().createRoom({
|
MatrixClientPeg.get().createRoom({
|
||||||
invite: [this.props.member.userId],
|
invite: [this.props.member.userId],
|
||||||
preset: "private_chat"
|
preset: "private_chat"
|
||||||
}).done(function(res) {
|
}).done(
|
||||||
self.setState({ creatingRoom: false });
|
function(res) {
|
||||||
dis.dispatch({
|
self.setState({ creatingRoom: false });
|
||||||
action: 'view_room',
|
dis.dispatch({
|
||||||
room_id: res.room_id
|
action: 'view_room',
|
||||||
});
|
room_id: res.room_id
|
||||||
self.props.onFinished();
|
});
|
||||||
}, function(err) {
|
self.props.onFinished();
|
||||||
self.setState({ creatingRoom: false });
|
}, function(err) {
|
||||||
console.error(
|
self.setState({ creatingRoom: false });
|
||||||
"Failed to create room: %s", JSON.stringify(err)
|
console.error(
|
||||||
);
|
"Failed to create room: %s", JSON.stringify(err)
|
||||||
self.props.onFinished();
|
);
|
||||||
});
|
self.props.onFinished();
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -291,9 +330,15 @@ module.exports = React.createClass({
|
||||||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||||
powerLevels.state_default
|
powerLevels.state_default
|
||||||
);
|
);
|
||||||
|
var levelToSend = (
|
||||||
|
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
||||||
|
powerLevels.events_default
|
||||||
|
);
|
||||||
|
|
||||||
can.kick = me.powerLevel >= powerLevels.kick;
|
can.kick = me.powerLevel >= powerLevels.kick;
|
||||||
can.ban = me.powerLevel >= powerLevels.ban;
|
can.ban = me.powerLevel >= powerLevels.ban;
|
||||||
can.mute = me.powerLevel >= editPowerLevel;
|
can.mute = me.powerLevel >= editPowerLevel;
|
||||||
|
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
||||||
can.modifyLevel = me.powerLevel > them.powerLevel;
|
can.modifyLevel = me.powerLevel > them.powerLevel;
|
||||||
return can;
|
return can;
|
||||||
},
|
},
|
||||||
|
@ -317,12 +362,11 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
|
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||||
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
|
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
||||||
interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
|
// FIXME: we're referring to a vector component from react-sdk
|
||||||
}
|
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
|
||||||
else {
|
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
|
||||||
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.creatingRoom) {
|
if (this.state.creatingRoom) {
|
||||||
|
@ -346,35 +390,56 @@ module.exports = React.createClass({
|
||||||
{muteLabel}
|
{muteLabel}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
if (this.state.can.modifyLevel) {
|
if (this.state.can.toggleMod) {
|
||||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
|
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||||
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||||
{giveOpLabel}
|
{giveOpLabel}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
|
||||||
|
// e.g. clicking on a linkified userid in a room
|
||||||
|
|
||||||
|
var adminTools;
|
||||||
|
if (kickButton || banButton || muteButton || giveModButton) {
|
||||||
|
adminTools =
|
||||||
|
<div>
|
||||||
|
<h3>Admin tools</h3>
|
||||||
|
|
||||||
|
<div className="mx_MemberInfo_buttons">
|
||||||
|
{muteButton}
|
||||||
|
{kickButton}
|
||||||
|
{banButton}
|
||||||
|
{giveModButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberInfo">
|
<div className="mx_MemberInfo">
|
||||||
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
||||||
<div className="mx_MemberInfo_avatar">
|
<div className="mx_MemberInfo_avatar">
|
||||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>{ this.props.member.name }</h2>
|
<h2>{ this.props.member.name }</h2>
|
||||||
<div className="mx_MemberInfo_profileField">
|
|
||||||
{ this.props.member.userId }
|
<div className="mx_MemberInfo_profile">
|
||||||
</div>
|
<div className="mx_MemberInfo_profileField">
|
||||||
<div className="mx_MemberInfo_profileField">
|
{ this.props.member.userId }
|
||||||
power: { this.props.member.powerLevelNorm }%
|
</div>
|
||||||
</div>
|
<div className="mx_MemberInfo_profileField">
|
||||||
<div className="mx_MemberInfo_buttons">
|
Level: <b><PowerSelector value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
|
||||||
{interactButton}
|
</div>
|
||||||
{muteButton}
|
|
||||||
{kickButton}
|
|
||||||
{banButton}
|
|
||||||
{giveModButton}
|
|
||||||
{spinner}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{ startChat }
|
||||||
|
|
||||||
|
{ adminTools }
|
||||||
|
|
||||||
|
{ spinner }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var classNames = require('classnames');
|
var classNames = require('classnames');
|
||||||
|
var Matrix = require("matrix-js-sdk");
|
||||||
|
var q = require('q');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
var Modal = require("../../../Modal");
|
var Modal = require("../../../Modal");
|
||||||
|
var Entities = require("../../../Entities");
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
|
||||||
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
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({
|
module.exports = React.createClass({
|
||||||
displayName: 'MemberList',
|
displayName: 'MemberList',
|
||||||
|
@ -63,8 +71,13 @@ module.exports = React.createClass({
|
||||||
self.setState({
|
self.setState({
|
||||||
members: self.roomMembers()
|
members: self.roomMembers()
|
||||||
});
|
});
|
||||||
|
// lazy load to prevent it blocking the first render
|
||||||
|
self._loadUserList();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout
|
||||||
|
|
||||||
// Attach a SINGLE listener for global presence changes then locate the
|
// Attach a SINGLE listener for global presence changes then locate the
|
||||||
// member tile and re-render it. This is more efficient than every tile
|
// member tile and re-render it. This is more efficient than every tile
|
||||||
// evar attaching their own listener.
|
// evar attaching their own listener.
|
||||||
|
@ -88,6 +101,21 @@ module.exports = React.createClass({
|
||||||
/*componentWillReceiveProps: function(newProps) {
|
/*componentWillReceiveProps: function(newProps) {
|
||||||
},*/
|
},*/
|
||||||
|
|
||||||
|
_loadUserList: function() {
|
||||||
|
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
|
if (!room) {
|
||||||
|
return; // we'll do it later
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the complete user list for inviting new users
|
||||||
|
// TODO: Keep this list bleeding-edge up-to-date. Practically speaking,
|
||||||
|
// it will do for now not being updated as random new users join different
|
||||||
|
// rooms as this list will be reloaded every room swap.
|
||||||
|
this.userList = MatrixClientPeg.get().getUsers().filter(function(u) {
|
||||||
|
return !room.hasMembershipState(u.userId, "join");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
if (room.roomId !== this.props.roomId) {
|
if (room.roomId !== this.props.roomId) {
|
||||||
return;
|
return;
|
||||||
|
@ -96,6 +124,7 @@ module.exports = React.createClass({
|
||||||
// we need to wait till the room is fully populated with state
|
// we need to wait till the room is fully populated with state
|
||||||
// before refreshing the member list else we get a stale list.
|
// before refreshing the member list else we get a stale list.
|
||||||
this._updateList();
|
this._updateList();
|
||||||
|
this._loadUserList();
|
||||||
},
|
},
|
||||||
|
|
||||||
onRoomStateMember: function(ev, state, member) {
|
onRoomStateMember: function(ev, state, member) {
|
||||||
|
@ -131,12 +160,41 @@ module.exports = React.createClass({
|
||||||
return;
|
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) {
|
if (isEmailAddress) {
|
||||||
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText);
|
promise = promise.then(function() {
|
||||||
|
MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText);
|
promise = promise.then(function() {
|
||||||
|
MatrixClientPeg.get().invite(self.props.roomId, inputText);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setState({
|
self.setState({
|
||||||
|
@ -151,11 +209,13 @@ module.exports = React.createClass({
|
||||||
inviting: false
|
inviting: false
|
||||||
});
|
});
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
if (err !== null) {
|
||||||
Modal.createDialog(ErrorDialog, {
|
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||||
title: "Server error whilst inviting",
|
Modal.createDialog(ErrorDialog, {
|
||||||
description: err.message
|
title: "Server error whilst inviting",
|
||||||
});
|
description: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
self.setState({
|
self.setState({
|
||||||
inviting: false
|
inviting: false
|
||||||
});
|
});
|
||||||
|
@ -225,12 +285,23 @@ module.exports = React.createClass({
|
||||||
return latB - latA;
|
return latB - latA;
|
||||||
},
|
},
|
||||||
|
|
||||||
makeMemberTiles: function(membership) {
|
onSearchQueryChanged: function(input) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: input
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
makeMemberTiles: function(membership, query) {
|
||||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||||
|
query = (query || "").toLowerCase();
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
return self.state.members.filter(function(userId) {
|
|
||||||
|
var memberList = self.state.members.filter(function(userId) {
|
||||||
var m = self.memberDict[userId];
|
var m = self.memberDict[userId];
|
||||||
|
if (query && m.name.toLowerCase().indexOf(query) !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return m.membership == membership;
|
return m.membership == membership;
|
||||||
}).map(function(userId) {
|
}).map(function(userId) {
|
||||||
var m = self.memberDict[userId];
|
var m = self.memberDict[userId];
|
||||||
|
@ -238,11 +309,32 @@ module.exports = React.createClass({
|
||||||
<MemberTile key={userId} member={m} ref={userId} />
|
<MemberTile key={userId} member={m} ref={userId} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
onPopulateInvite: function(e) {
|
if (membership === "invite") {
|
||||||
this.onInvite(this.refs.invite.value);
|
// include 3pid invites (m.room.third_party_invite) state events.
|
||||||
e.preventDefault();
|
// 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);
|
||||||
|
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memberList.push(
|
||||||
|
<EntityTile key={e.getStateKey()} ref={e.getStateKey()}
|
||||||
|
name={e.getContent().display_name} />
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberList;
|
||||||
},
|
},
|
||||||
|
|
||||||
inviteTile: function() {
|
inviteTile: function() {
|
||||||
|
@ -252,22 +344,25 @@ module.exports = React.createClass({
|
||||||
<Loader />
|
<Loader />
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
var SearchableEntityList = sdk.getComponent("rooms.SearchableEntityList");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onPopulateInvite}>
|
<SearchableEntityList searchPlaceholderText={"Invite / Search"}
|
||||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite user (email)"/>
|
onSubmit={this.onInvite}
|
||||||
</form>
|
onQueryChanged={this.onSearchQueryChanged}
|
||||||
|
entities={Entities.fromUsers(this.userList || [], true, this.onInvite)} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var invitedSection = null;
|
var invitedSection = null;
|
||||||
var invitedMemberTiles = this.makeMemberTiles('invite');
|
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
|
||||||
if (invitedMemberTiles.length > 0) {
|
if (invitedMemberTiles.length > 0) {
|
||||||
invitedSection = (
|
invitedSection = (
|
||||||
<div className="mx_MemberList_invited">
|
<div className="mx_MemberList_invited">
|
||||||
<h2>Invited</h2>
|
<h2>Invited</h2>
|
||||||
<div className="mx_MemberList_wrapper">
|
<div autoshow={true} className="mx_MemberList_wrapper">
|
||||||
{invitedMemberTiles}
|
{invitedMemberTiles}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -275,15 +370,17 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="mx_MemberList">
|
<div className="mx_MemberList">
|
||||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_border">
|
|
||||||
{this.inviteTile()}
|
{this.inviteTile()}
|
||||||
<div>
|
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||||
<div className="mx_MemberList_wrapper">
|
<div className="mx_MemberList_wrapper">
|
||||||
{this.makeMemberTiles('join')}
|
{this.makeMemberTiles('join', this.state.searchQuery)}
|
||||||
|
</div>
|
||||||
|
{invitedSection}
|
||||||
|
</GeminiScrollbar>
|
||||||
|
<div className="mx_MemberList_bottom">
|
||||||
|
<div className="mx_MemberList_bottomRule">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{invitedSection}
|
|
||||||
</GeminiScrollbar>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,25 +26,20 @@ var Modal = require("../../../Modal");
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MemberTile',
|
displayName: 'MemberTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
member: React.PropTypes.any.isRequired, // RoomMember
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
onLeaveClick: function() {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'leave_room',
|
|
||||||
room_id: this.props.member.roomId,
|
|
||||||
});
|
|
||||||
this.props.onFinished();
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
if (this.state.hover !== nextState.hover) return true;
|
|
||||||
if (
|
if (
|
||||||
this.member_last_modified_time === undefined ||
|
this.member_last_modified_time === undefined ||
|
||||||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
||||||
) {
|
) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
nextProps.member.user &&
|
nextProps.member.user &&
|
||||||
|
@ -56,14 +51,6 @@ module.exports = React.createClass({
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
mouseEnter: function(e) {
|
|
||||||
this.setState({ 'hover': true });
|
|
||||||
},
|
|
||||||
|
|
||||||
mouseLeave: function(e) {
|
|
||||||
this.setState({ 'hover': false });
|
|
||||||
},
|
|
||||||
|
|
||||||
onClick: function(e) {
|
onClick: function(e) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_user',
|
action: 'view_user',
|
||||||
|
@ -71,114 +58,43 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getDuration: function(time) {
|
_getDisplayName: function() {
|
||||||
if (!time) return;
|
return this.props.member.name;
|
||||||
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() {
|
getPowerLabel: function() {
|
||||||
var label = this.props.member.userId;
|
return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
|
||||||
if (this.state.isTargetMod) {
|
|
||||||
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
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 = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
|
|
||||||
// }
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
|
|
||||||
|
|
||||||
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 = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) } { this.getDuration(active) } ago</div>;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
presence = <div className="mx_MemberTile_presence">{ this.getPrettyPresence(this.props.member.user) }</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
nameEl =
|
|
||||||
<div className="mx_MemberTile_details">
|
|
||||||
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
|
|
||||||
<div className="mx_MemberTile_userId">{ name }</div>
|
|
||||||
{ presence }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
nameEl =
|
|
||||||
<div className="mx_MemberTile_name">
|
|
||||||
{ name }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
var EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||||
|
|
||||||
|
var member = this.props.member;
|
||||||
|
var name = this._getDisplayName();
|
||||||
|
var active = -1;
|
||||||
|
var presenceState = member.user ? member.user.presence : null;
|
||||||
|
|
||||||
|
var av = (
|
||||||
|
<MemberAvatar member={member} width={36} height={36} />
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.member_last_modified_time = member.getLastModifiedTime();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={mainClassName} title={ this.getPowerLabel() }
|
<EntityTile {...this.props} presenceActiveAgo={active} presenceState={presenceState}
|
||||||
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
|
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||||
onMouseLeave={ this.mouseLeave }>
|
shouldComponentUpdate={this.shouldComponentUpdate.bind(this)}
|
||||||
<div className="mx_MemberTile_avatar">
|
name={name} powerLevel={this.props.member.powerLevel} />
|
||||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
|
||||||
{ power }
|
|
||||||
</div>
|
|
||||||
{ nameEl }
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,8 +65,17 @@ function mdownToHtml(mdown) {
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MessageComposer',
|
displayName: 'MessageComposer',
|
||||||
|
|
||||||
|
statics: {
|
||||||
|
// the height we limit the composer to
|
||||||
|
MAX_HEIGHT: 100,
|
||||||
|
},
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
tabComplete: React.PropTypes.any
|
tabComplete: React.PropTypes.any,
|
||||||
|
|
||||||
|
// a callback which is called when the height of the composer is
|
||||||
|
// changed due to a change in content.
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
@ -200,23 +209,18 @@ module.exports = React.createClass({
|
||||||
this.sentHistory.push(input);
|
this.sentHistory.push(input);
|
||||||
this.onEnter(ev);
|
this.onEnter(ev);
|
||||||
}
|
}
|
||||||
else if (ev.keyCode === KeyCode.UP) {
|
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||||
var input = this.refs.textarea.value;
|
var oldSelectionStart = this.refs.textarea.selectionStart;
|
||||||
var offset = this.refs.textarea.selectionStart || 0;
|
// Remember the keyCode because React will recycle the synthetic event
|
||||||
if (ev.ctrlKey || !input.substr(0, offset).match(/\n/)) {
|
var keyCode = ev.keyCode;
|
||||||
this.sentHistory.next(1);
|
// set a callback so we can see if the cursor position changes as
|
||||||
ev.preventDefault();
|
// a result of this event. If it doesn't, we cycle history.
|
||||||
this.resizeInput();
|
setTimeout(() => {
|
||||||
}
|
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
||||||
}
|
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
||||||
else if (ev.keyCode === KeyCode.DOWN) {
|
this.resizeInput();
|
||||||
var input = this.refs.textarea.value;
|
}
|
||||||
var offset = this.refs.textarea.selectionStart || 0;
|
}, 0);
|
||||||
if (ev.ctrlKey || !input.substr(offset).match(/\n/)) {
|
|
||||||
this.sentHistory.next(-1);
|
|
||||||
ev.preventDefault();
|
|
||||||
this.resizeInput();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.tabComplete) {
|
if (this.props.tabComplete) {
|
||||||
|
@ -237,13 +241,15 @@ module.exports = React.createClass({
|
||||||
// scrollHeight is at least equal to clientHeight, so we have to
|
// scrollHeight is at least equal to clientHeight, so we have to
|
||||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||||
this.refs.textarea.style.height = "0px";
|
this.refs.textarea.style.height = "0px";
|
||||||
var newHeight = this.refs.textarea.scrollHeight < 100 ? this.refs.textarea.scrollHeight : 100;
|
var newHeight = Math.min(this.refs.textarea.scrollHeight,
|
||||||
|
this.constructor.MAX_HEIGHT);
|
||||||
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
this.refs.textarea.style.height = Math.ceil(newHeight) + "px";
|
||||||
if (this.props.roomView) {
|
|
||||||
// kick gemini-scrollbar to re-layout
|
|
||||||
this.props.roomView.forceUpdate();
|
|
||||||
}
|
|
||||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||||
|
|
||||||
|
if (this.props.onResize) {
|
||||||
|
// kick gemini-scrollbar to re-layout
|
||||||
|
this.props.onResize();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyUp: function(ev) {
|
onKeyUp: function(ev) {
|
||||||
|
@ -330,7 +336,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessagePromise.then(function() {
|
sendMessagePromise.done(function() {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'message_sent'
|
action: 'message_sent'
|
||||||
});
|
});
|
||||||
|
@ -461,6 +467,7 @@ module.exports = React.createClass({
|
||||||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||||
var uploadInputStyle = {display: 'none'};
|
var uploadInputStyle = {display: 'none'};
|
||||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||||
|
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
|
||||||
var callButton, videoCallButton, hangupButton;
|
var callButton, videoCallButton, hangupButton;
|
||||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||||
|
@ -473,12 +480,12 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
callButton =
|
callButton =
|
||||||
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}>
|
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
|
||||||
<img src="img/voice.svg" alt="Voice call" title="Voice call" width="16" height="26"/>
|
<TintableSvg src="img/voice.svg" width="16" height="26"/>
|
||||||
</div>
|
</div>
|
||||||
videoCallButton =
|
videoCallButton =
|
||||||
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
|
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
|
||||||
<img src="img/call.svg" alt="Video call" title="Video call" width="30" height="22"/>
|
<TintableSvg src="img/call.svg" width="30" height="22"/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,8 +499,8 @@ module.exports = React.createClass({
|
||||||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||||
<textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
<textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick}>
|
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick} title="Upload file">
|
||||||
<img src="img/upload.svg" alt="Upload file" title="Upload file" width="19" height="24"/>
|
<TintableSvg src="img/upload.svg" width="19" height="24"/>
|
||||||
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
|
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
|
||||||
</div>
|
</div>
|
||||||
{ hangupButton }
|
{ hangupButton }
|
||||||
|
|
84
src/components/views/rooms/PresenceLabel.js
Normal file
84
src/components/views/rooms/PresenceLabel.js
Normal file
|
@ -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 (
|
||||||
|
<div className="mx_PresenceLabel">
|
||||||
|
{ this.getPrettyPresence(this.props.presenceState) } { this.getDuration(this.props.activeAgo) } ago
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div className="mx_PresenceLabel">
|
||||||
|
{ this.getPrettyPresence(this.props.presenceState) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -21,6 +21,12 @@ var sdk = require('../../../index');
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
|
||||||
|
var linkify = require('linkifyjs');
|
||||||
|
var linkifyElement = require('linkifyjs/element');
|
||||||
|
var linkifyMatrix = require('../../../linkify-matrix');
|
||||||
|
|
||||||
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomHeader',
|
displayName: 'RoomHeader',
|
||||||
|
|
||||||
|
@ -41,6 +47,25 @@ module.exports = React.createClass({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillReceiveProps: function(newProps) {
|
||||||
|
if (newProps.editing) {
|
||||||
|
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||||
|
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
name: name ? name.getContent().name : '',
|
||||||
|
defaultName: this.props.room.getDefaultRoomName(MatrixClientPeg.get().credentials.userId),
|
||||||
|
topic: topic ? topic.getContent().topic : '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate: function() {
|
||||||
|
if (this.refs.topic) {
|
||||||
|
linkifyElement(this.refs.topic, linkifyMatrix.options);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onVideoClick: function(e) {
|
onVideoClick: function(e) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
|
@ -57,25 +82,59 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onNameChange: function(new_name) {
|
onNameChanged: function(value) {
|
||||||
if (this.props.room.name != new_name && new_name) {
|
this.setState({ name : value });
|
||||||
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
|
},
|
||||||
|
|
||||||
|
onTopicChanged: function(value) {
|
||||||
|
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() {
|
getRoomName: function() {
|
||||||
return this.refs.name_edit.value;
|
return this.state.name;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTopic: function() {
|
||||||
|
return this.state.topic;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var EditableText = sdk.getComponent("elements.EditableText");
|
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;
|
var header;
|
||||||
if (this.props.simpleHeader) {
|
if (this.props.simpleHeader) {
|
||||||
var cancel;
|
var cancel;
|
||||||
if (this.props.onCancelClick) {
|
if (this.props.onCancelClick) {
|
||||||
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel-black.png" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
|
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel.svg" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
|
||||||
}
|
}
|
||||||
header =
|
header =
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper">
|
||||||
|
@ -86,27 +145,72 @@ module.exports = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
|
||||||
|
|
||||||
var name = null;
|
var name = null;
|
||||||
var searchStatus = null;
|
var searchStatus = null;
|
||||||
var topic_el = null;
|
var topic_el = null;
|
||||||
var cancel_button = null;
|
var cancel_button = null;
|
||||||
var save_button = null;
|
var save_button = null;
|
||||||
var settings_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) {
|
if (this.props.editing) {
|
||||||
name =
|
|
||||||
<div className="mx_RoomHeader_nameEditing">
|
|
||||||
<input className="mx_RoomHeader_nameInput" type="text" defaultValue={actual_name} placeholder="Name" ref="name_edit"/>
|
|
||||||
</div>
|
|
||||||
// if (topic) topic_el = <div className="mx_RoomHeader_topic"><textarea>{ topic.getContent().topic }</textarea></div>
|
|
||||||
cancel_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onCancelClick}>Cancel</div>
|
|
||||||
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save Changes</div>
|
|
||||||
} else {
|
|
||||||
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
|
|
||||||
|
|
||||||
|
// 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 ? 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 state_default = parseInt((power_levels ? power_levels.state_default : 0) || 0);
|
||||||
|
|
||||||
|
var room_avatar_level = state_default;
|
||||||
|
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 = state_default;
|
||||||
|
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 = state_default;
|
||||||
|
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 !== 'Empty room') {
|
||||||
|
placeholderName += " (" + this.state.defaultName + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save</div>
|
||||||
|
cancel_button = <div className="mx_RoomHeader_cancelButton" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" alt="Cancel"/> </div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (can_set_room_name) {
|
||||||
|
name =
|
||||||
|
<div className="mx_RoomHeader_name">
|
||||||
|
<EditableText
|
||||||
|
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
|
||||||
|
placeholderClassName="mx_RoomHeader_placeholder"
|
||||||
|
placeholder={ placeholderName }
|
||||||
|
blurToCancel={ false }
|
||||||
|
onValueChanged={ this.onNameChanged }
|
||||||
|
initialValue={ this.state.name }/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
var searchStatus;
|
var searchStatus;
|
||||||
// don't display the search count until the search completes and
|
// don't display the search count until the search completes and
|
||||||
// gives us a valid (possibly zero) searchCount.
|
// gives us a valid (possibly zero) searchCount.
|
||||||
|
@ -114,39 +218,93 @@ module.exports = React.createClass({
|
||||||
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
searchStatus = <div className="mx_RoomHeader_searchStatus"> (~{ this.props.searchInfo.searchCount } results)</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||||
|
var members = this.props.room.getJoinedMembers();
|
||||||
|
var settingsHint = false;
|
||||||
|
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||||
|
var name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||||
|
if (!name || !name.getContent().name) {
|
||||||
|
settingsHint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
name =
|
name =
|
||||||
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
|
||||||
<div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div>
|
<div className={ "mx_RoomHeader_nametext " + (settingsHint ? "mx_RoomHeader_settingsHint" : "") } title={ this.props.room.name }>{ this.props.room.name }</div>
|
||||||
{ searchStatus }
|
{ searchStatus }
|
||||||
<div className="mx_RoomHeader_settingsButton">
|
<div className="mx_RoomHeader_settingsButton" title="Settings">
|
||||||
<img src="img/settings.svg" width="12" height="12"/>
|
<TintableSvg src="img/settings.svg" width="12" height="12"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
|
}
|
||||||
|
|
||||||
|
if (can_set_room_topic) {
|
||||||
|
topic_el =
|
||||||
|
<EditableText
|
||||||
|
className="mx_RoomHeader_topic mx_RoomHeader_editable"
|
||||||
|
placeholderClassName="mx_RoomHeader_placeholder"
|
||||||
|
placeholder="Add a topic"
|
||||||
|
blurToCancel={ false }
|
||||||
|
onValueChanged={ this.onTopicChanged }
|
||||||
|
initialValue={ this.state.topic }/>
|
||||||
|
} else {
|
||||||
|
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||||
|
if (topic) topic_el = <div className="mx_RoomHeader_topic" ref="topic" title={ topic.getContent().topic }>{ topic.getContent().topic }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomAvatar = null;
|
var roomAvatar = null;
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
roomAvatar = (
|
if (can_set_room_avatar) {
|
||||||
<RoomAvatar room={this.props.room} width="48" height="48" />
|
roomAvatar = (
|
||||||
);
|
<div className="mx_RoomHeader_avatarPicker">
|
||||||
|
<div onClick={ this.onAvatarPickerClick }>
|
||||||
|
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomHeader_avatarPicker_edit">
|
||||||
|
<label htmlFor="avatarInput" ref="file_label">
|
||||||
|
<img src="img/camera.svg"
|
||||||
|
alt="Upload avatar" title="Upload avatar"
|
||||||
|
width="17" height="15" />
|
||||||
|
</label>
|
||||||
|
<input id="avatarInput" type="file" onChange={ this.onAvatarSelected }/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
roomAvatar = (
|
||||||
|
<div onClick={this.props.onSettingsClick}>
|
||||||
|
<RoomAvatar room={this.props.room} width={48} height={48}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var leave_button;
|
var leave_button;
|
||||||
if (this.props.onLeaveClick) {
|
if (this.props.onLeaveClick) {
|
||||||
leave_button =
|
leave_button =
|
||||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
|
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
|
||||||
<img src="img/leave.svg" title="Leave room" alt="Leave room"
|
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||||
width="26" height="20" onClick={this.props.onLeaveClick}/>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
var forget_button;
|
var forget_button;
|
||||||
if (this.props.onForgetClick) {
|
if (this.props.onForgetClick) {
|
||||||
forget_button =
|
forget_button =
|
||||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
|
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
|
||||||
<img src="img/leave.svg" title="Forget room" alt="Forget room"
|
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||||
width="26" height="20" onClick={this.props.onForgetClick}/>
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var right_row;
|
||||||
|
if (!this.props.editing) {
|
||||||
|
right_row =
|
||||||
|
<div className="mx_RoomHeader_rightRow">
|
||||||
|
{ forget_button }
|
||||||
|
{ leave_button }
|
||||||
|
<div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search">
|
||||||
|
<TintableSvg src="img/search.svg" width="21" height="19"/>
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,20 +319,14 @@ module.exports = React.createClass({
|
||||||
{ topic_el }
|
{ topic_el }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{cancel_button}
|
|
||||||
{save_button}
|
{save_button}
|
||||||
<div className="mx_RoomHeader_rightRow">
|
{cancel_button}
|
||||||
{ forget_button }
|
{right_row}
|
||||||
{ leave_button }
|
|
||||||
<div className="mx_RoomHeader_button">
|
|
||||||
<img src="img/search.svg" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomHeader">
|
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
||||||
{ header }
|
{ header }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
83
src/components/views/rooms/RoomPreviewBar.js
Normal file
83
src/components/views/rooms/RoomPreviewBar.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
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,
|
||||||
|
onRejectClick: React.PropTypes.func,
|
||||||
|
inviterName: React.PropTypes.string,
|
||||||
|
canJoin: React.PropTypes.bool,
|
||||||
|
canPreview: React.PropTypes.bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
onJoinClick: function() {},
|
||||||
|
canJoin: false,
|
||||||
|
canPreview: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var joinBlock, previewBlock;
|
||||||
|
|
||||||
|
if (this.props.inviterName) {
|
||||||
|
joinBlock = (
|
||||||
|
<div>
|
||||||
|
<div className="mx_RoomPreviewBar_invite_text">
|
||||||
|
You have been invited to join this room by { this.props.inviterName }
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomPreviewBar_join_text">
|
||||||
|
Would you like to <a onClick={ this.props.onJoinClick }>accept</a> or <a onClick={ this.props.onRejectClick }>decline</a> this invitation?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (this.props.canJoin) {
|
||||||
|
joinBlock = (
|
||||||
|
<div>
|
||||||
|
<div className="mx_RoomPreviewBar_join_text">
|
||||||
|
Would you like to <a onClick={ this.props.onJoinClick }>join</a> this room?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.canPreview) {
|
||||||
|
previewBlock = (
|
||||||
|
<div className="mx_RoomPreviewBar_preview_text">
|
||||||
|
This is a preview of this room. Room interactions have been disabled.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomPreviewBar">
|
||||||
|
<div className="mx_RoomPreviewBar_wrapper">
|
||||||
|
{ joinBlock }
|
||||||
|
{ previewBlock }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -16,23 +16,100 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var Tinter = require('../../../Tinter');
|
||||||
var sdk = require('../../../index');
|
var sdk = require('../../../index');
|
||||||
|
var Modal = require('../../../Modal');
|
||||||
|
|
||||||
|
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({
|
module.exports = React.createClass({
|
||||||
displayName: 'RoomSettings',
|
displayName: 'RoomSettings',
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
room: React.PropTypes.object.isRequired,
|
room: React.PropTypes.object.isRequired,
|
||||||
|
onSaveClick: React.PropTypes.func,
|
||||||
|
onCancelClick: React.PropTypes.func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
// work out the initial color index
|
||||||
|
var room_color_index = undefined;
|
||||||
|
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();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the aliases
|
||||||
|
var aliases = {};
|
||||||
|
var domain = MatrixClientPeg.get().getDomain();
|
||||||
|
var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases');
|
||||||
|
for (var i = 0; i < alias_events.length; i++) {
|
||||||
|
aliases[alias_events[i].getStateKey()] = alias_events[i].getContent().aliases.slice(); // shallow copy
|
||||||
|
}
|
||||||
|
aliases[domain] = aliases[domain] || [];
|
||||||
|
|
||||||
|
var tags = {};
|
||||||
|
Object.keys(this.props.room.tags).forEach(function(tagName) {
|
||||||
|
tags[tagName] = {};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
power_levels_changed: false
|
power_levels_changed: false,
|
||||||
|
color_scheme_changed: false,
|
||||||
|
color_scheme_index: room_color_index,
|
||||||
|
aliases_changed: false,
|
||||||
|
aliases: aliases,
|
||||||
|
tags_changed: false,
|
||||||
|
tags: tags,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
resetState: function() {
|
||||||
|
this.set.state(this.getInitialState());
|
||||||
|
},
|
||||||
|
|
||||||
|
canGuestsJoin: function() {
|
||||||
|
return this.refs.guests_join.checked;
|
||||||
|
},
|
||||||
|
|
||||||
|
canGuestsRead: function() {
|
||||||
|
return this.refs.guests_read.checked;
|
||||||
|
},
|
||||||
|
|
||||||
getTopic: function() {
|
getTopic: function() {
|
||||||
return this.refs.topic.value;
|
return this.refs.topic ? this.refs.topic.value : "";
|
||||||
},
|
},
|
||||||
|
|
||||||
getJoinRules: function() {
|
getJoinRules: function() {
|
||||||
|
@ -43,6 +120,10 @@ module.exports = React.createClass({
|
||||||
return this.refs.share_history.checked ? "shared" : "invited";
|
return this.refs.share_history.checked ? "shared" : "invited";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
areNotificationsMuted: function() {
|
||||||
|
return this.refs.are_notifications_muted.checked;
|
||||||
|
},
|
||||||
|
|
||||||
getPowerLevels: function() {
|
getPowerLevels: function() {
|
||||||
if (!this.state.power_levels_changed) return undefined;
|
if (!this.state.power_levels_changed) return undefined;
|
||||||
|
|
||||||
|
@ -50,13 +131,13 @@ module.exports = React.createClass({
|
||||||
power_levels = power_levels.getContent();
|
power_levels = power_levels.getContent();
|
||||||
|
|
||||||
var new_power_levels = {
|
var new_power_levels = {
|
||||||
ban: parseInt(this.refs.ban.value),
|
ban: parseInt(this.refs.ban.getValue()),
|
||||||
kick: parseInt(this.refs.kick.value),
|
kick: parseInt(this.refs.kick.getValue()),
|
||||||
redact: parseInt(this.refs.redact.value),
|
redact: parseInt(this.refs.redact.getValue()),
|
||||||
invite: parseInt(this.refs.invite.value),
|
invite: parseInt(this.refs.invite.getValue()),
|
||||||
events_default: parseInt(this.refs.events_default.value),
|
events_default: parseInt(this.refs.events_default.getValue()),
|
||||||
state_default: parseInt(this.refs.state_default.value),
|
state_default: parseInt(this.refs.state_default.getValue()),
|
||||||
users_default: parseInt(this.refs.users_default.value),
|
users_default: parseInt(this.refs.users_default.getValue()),
|
||||||
users: power_levels.users,
|
users: power_levels.users,
|
||||||
events: power_levels.events,
|
events: power_levels.events,
|
||||||
};
|
};
|
||||||
|
@ -64,17 +145,231 @@ module.exports = React.createClass({
|
||||||
return new_power_levels;
|
return new_power_levels;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCanonicalAlias: function() {
|
||||||
|
return this.refs.canonical_alias ? this.refs.canonical_alias.value : "";
|
||||||
|
},
|
||||||
|
|
||||||
|
getAliasOperations: function() {
|
||||||
|
if (!this.state.aliases_changed) return undefined;
|
||||||
|
|
||||||
|
// work out the delta from room state to UI state
|
||||||
|
var ops = [];
|
||||||
|
|
||||||
|
// calculate original ("old") aliases
|
||||||
|
var oldAliases = {};
|
||||||
|
var aliases = this.state.aliases;
|
||||||
|
var alias_events = this.props.room.currentState.getStateEvents('m.room.aliases');
|
||||||
|
for (var i = 0; i < alias_events.length; i++) {
|
||||||
|
var domain = alias_events[i].getStateKey();
|
||||||
|
oldAliases[domain] = alias_events[i].getContent().aliases.slice(); // shallow copy
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: this whole delta-based set comparison function used for domains, aliases & tags
|
||||||
|
// should be factored out asap rather than duplicated like this.
|
||||||
|
|
||||||
|
// work out whether any domains have entirely disappeared or appeared
|
||||||
|
var domainDelta = {}
|
||||||
|
Object.keys(oldAliases).forEach(function(domain) {
|
||||||
|
domainDelta[domain] = domainDelta[domain] || 0;
|
||||||
|
domainDelta[domain]--;
|
||||||
|
});
|
||||||
|
Object.keys(aliases).forEach(function(domain) {
|
||||||
|
domainDelta[domain] = domainDelta[domain] || 0;
|
||||||
|
domainDelta[domain]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(domainDelta).forEach(function(domain) {
|
||||||
|
switch (domainDelta[domain]) {
|
||||||
|
case 1: // entirely new domain
|
||||||
|
aliases[domain].forEach(function(alias) {
|
||||||
|
ops.push({ type: "put", alias : alias });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case -1: // entirely removed domain
|
||||||
|
oldAliases[domain].forEach(function(alias) {
|
||||||
|
ops.push({ type: "delete", alias : alias });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 0: // mix of aliases in this domain.
|
||||||
|
// compare old & new aliases for this domain
|
||||||
|
var delta = {};
|
||||||
|
oldAliases[domain].forEach(function(item) {
|
||||||
|
delta[item] = delta[item] || 0;
|
||||||
|
delta[item]--;
|
||||||
|
});
|
||||||
|
aliases[domain].forEach(function(item) {
|
||||||
|
delta[item] = delta[item] || 0;
|
||||||
|
delta[item]++;
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(delta).forEach(function(alias) {
|
||||||
|
if (delta[alias] == 1) {
|
||||||
|
ops.push({ type: "put", alias: alias });
|
||||||
|
} else if (delta[alias] == -1) {
|
||||||
|
ops.push({ type: "delete", alias: alias });
|
||||||
|
} else {
|
||||||
|
console.error("Calculated alias delta of " + delta[alias] +
|
||||||
|
" - this should never happen!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error("Calculated domain delta of " + domainDelta[domain] +
|
||||||
|
" - this should never happen!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ops;
|
||||||
|
},
|
||||||
|
|
||||||
|
getTagOperations: function() {
|
||||||
|
if (!this.state.tags_changed) return undefined;
|
||||||
|
|
||||||
|
var ops = [];
|
||||||
|
|
||||||
|
var delta = {};
|
||||||
|
Object.keys(this.props.room.tags).forEach(function(oldTag) {
|
||||||
|
delta[oldTag] = delta[oldTag] || 0;
|
||||||
|
delta[oldTag]--;
|
||||||
|
});
|
||||||
|
Object.keys(this.state.tags).forEach(function(newTag) {
|
||||||
|
delta[newTag] = delta[newTag] || 0;
|
||||||
|
delta[newTag]++;
|
||||||
|
});
|
||||||
|
Object.keys(delta).forEach(function(tag) {
|
||||||
|
if (delta[tag] == 1) {
|
||||||
|
ops.push({ type: "put", tag: tag });
|
||||||
|
} else if (delta[tag] == -1) {
|
||||||
|
ops.push({ type: "delete", tag: tag });
|
||||||
|
} else {
|
||||||
|
console.error("Calculated tag delta of " + delta[tag] +
|
||||||
|
" - this should never happen!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ops;
|
||||||
|
},
|
||||||
|
|
||||||
onPowerLevelsChanged: function() {
|
onPowerLevelsChanged: function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
power_levels_changed: true
|
power_levels_changed: true
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
getColorScheme: function() {
|
||||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
if (!this.state.color_scheme_changed) return undefined;
|
||||||
|
|
||||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
return {
|
||||||
if (topic) topic = topic.getContent().topic;
|
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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onAliasChanged: function(domain, index, alias) {
|
||||||
|
if (alias === "") return; // hit the delete button to delete please
|
||||||
|
var oldAlias;
|
||||||
|
if (this.isAliasValid(alias)) {
|
||||||
|
oldAlias = this.state.aliases[domain][index];
|
||||||
|
this.state.aliases[domain][index] = alias;
|
||||||
|
this.setState({ aliases_changed : true });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Invalid address format",
|
||||||
|
description: "'" + alias + "' is not a valid format for an address",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAliasDeleted: function(domain, index) {
|
||||||
|
// It's a bit naughty to directly manipulate this.state, and React would
|
||||||
|
// normally whine at you, but it can't see us doing the splice. Given we
|
||||||
|
// promptly setState anyway, it's just about acceptable. The alternative
|
||||||
|
// would be to arbitrarily deepcopy to a temp variable and then setState
|
||||||
|
// that, but why bother when we can cut this corner.
|
||||||
|
var alias = this.state.aliases[domain].splice(index, 1);
|
||||||
|
this.setState({
|
||||||
|
aliases: this.state.aliases
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ aliases_changed : true });
|
||||||
|
},
|
||||||
|
|
||||||
|
onAliasAdded: function(alias) {
|
||||||
|
if (alias === "") return; // ignore attempts to create blank aliases
|
||||||
|
if (alias === undefined) {
|
||||||
|
alias = this.refs.add_alias ? this.refs.add_alias.getValue() : undefined;
|
||||||
|
if (alias === undefined || alias === "") return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAliasValid(alias)) {
|
||||||
|
var domain = alias.replace(/^.*?:/, '');
|
||||||
|
// XXX: do we need to deep copy aliases before editing it?
|
||||||
|
this.state.aliases[domain] = this.state.aliases[domain] || [];
|
||||||
|
this.state.aliases[domain].push(alias);
|
||||||
|
this.setState({
|
||||||
|
aliases: this.state.aliases
|
||||||
|
});
|
||||||
|
|
||||||
|
// reset the add field
|
||||||
|
this.refs.add_alias.setValue('');
|
||||||
|
|
||||||
|
this.setState({ aliases_changed : true });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Invalid alias format",
|
||||||
|
description: "'" + alias + "' is not a valid format for an alias",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isAliasValid: function(alias) {
|
||||||
|
// XXX: FIXME SPEC-1
|
||||||
|
return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias);
|
||||||
|
},
|
||||||
|
|
||||||
|
onTagChange: function(tagName, event) {
|
||||||
|
if (event.target.checked) {
|
||||||
|
if (tagName === 'm.favourite') {
|
||||||
|
delete this.state.tags['m.lowpriority'];
|
||||||
|
}
|
||||||
|
else if (tagName === 'm.lowpriority') {
|
||||||
|
delete this.state.tags['m.favourite'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.tags[tagName] = this.state.tags[tagName] || {};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
delete this.state.tags[tagName];
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: hacky say to deep-edit state
|
||||||
|
this.setState({
|
||||||
|
tags: this.state.tags,
|
||||||
|
tags_changed: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
// TODO: go through greying out things you don't have permission to change
|
||||||
|
// (or turning them into informative stuff)
|
||||||
|
|
||||||
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
|
var PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||||
|
|
||||||
var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
|
var join_rule = this.props.room.currentState.getStateEvents('m.room.join_rules', '');
|
||||||
if (join_rule) join_rule = join_rule.getContent().join_rule;
|
if (join_rule) join_rule = join_rule.getContent().join_rule;
|
||||||
|
@ -83,8 +378,23 @@ module.exports = React.createClass({
|
||||||
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
||||||
|
|
||||||
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
|
||||||
|
var guest_access = this.props.room.currentState.getStateEvents('m.room.guest_access', '');
|
||||||
|
if (guest_access) {
|
||||||
|
guest_access = guest_access.getContent().guest_access;
|
||||||
|
}
|
||||||
|
|
||||||
|
var are_notifications_muted;
|
||||||
|
var roomPushRule = MatrixClientPeg.get().getRoomPushRule("global", this.props.room.roomId);
|
||||||
|
if (roomPushRule) {
|
||||||
|
if (0 <= roomPushRule.actions.indexOf("dont_notify")) {
|
||||||
|
are_notifications_muted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var events_levels = (power_levels ? power_levels.events : {}) || {};
|
||||||
|
|
||||||
|
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||||
|
|
||||||
var events_levels = power_levels.events || {};
|
|
||||||
|
|
||||||
if (power_levels) {
|
if (power_levels) {
|
||||||
power_levels = power_levels.getContent();
|
power_levels = power_levels.getContent();
|
||||||
|
@ -103,8 +413,6 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
var user_levels = power_levels.users || {};
|
var user_levels = power_levels.users || {};
|
||||||
|
|
||||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
|
||||||
|
|
||||||
var current_user_level = user_levels[user_id];
|
var current_user_level = user_levels[user_id];
|
||||||
if (current_user_level == undefined) current_user_level = default_user_level;
|
if (current_user_level == undefined) current_user_level = default_user_level;
|
||||||
|
|
||||||
|
@ -133,104 +441,299 @@ module.exports = React.createClass({
|
||||||
var can_change_levels = false;
|
var can_change_levels = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var room_avatar_level = parseInt(power_levels.state_default || 0);
|
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
|
||||||
if (events_levels['m.room.avatar'] !== undefined) {
|
|
||||||
room_avatar_level = events_levels['m.room.avatar'];
|
var room_aliases_level = state_default;
|
||||||
}
|
if (events_levels['m.room.aliases'] !== undefined) {
|
||||||
var can_set_room_avatar = current_user_level >= room_avatar_level;
|
room_avatar_level = events_levels['m.room.aliases'];
|
||||||
|
}
|
||||||
|
var can_set_room_aliases = current_user_level >= room_aliases_level;
|
||||||
|
|
||||||
|
var canonical_alias_level = state_default;
|
||||||
|
if (events_levels['m.room.canonical_alias'] !== undefined) {
|
||||||
|
room_avatar_level = events_levels['m.room.canonical_alias'];
|
||||||
|
}
|
||||||
|
var can_set_canonical_alias = current_user_level >= canonical_alias_level;
|
||||||
|
|
||||||
|
var tag_level = state_default;
|
||||||
|
if (events_levels['m.tag'] !== undefined) {
|
||||||
|
tag_level = events_levels['m.tag'];
|
||||||
|
}
|
||||||
|
var can_set_tag = current_user_level >= tag_level;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var canonical_alias_event = this.props.room.currentState.getStateEvents('m.room.canonical_alias', '');
|
||||||
|
var canonical_alias = canonical_alias_event ? canonical_alias_event.getContent().alias : "";
|
||||||
|
var domain = MatrixClientPeg.get().getDomain();
|
||||||
|
|
||||||
|
var remote_domains = Object.keys(this.state.aliases).filter(function(alias) { return alias !== domain });
|
||||||
|
|
||||||
|
var remote_aliases_section;
|
||||||
|
if (remote_domains.length) {
|
||||||
|
remote_aliases_section =
|
||||||
|
<div>
|
||||||
|
<div className="mx_RoomSettings_aliasLabel">
|
||||||
|
Remote addresses for this room:
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_aliasesTable">
|
||||||
|
{ remote_domains.map(function(state_key, i) {
|
||||||
|
return self.state.aliases[state_key].map(function(alias, j) {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomSettings_aliasesTableRow" key={ i + "_" + j }>
|
||||||
|
<EditableText
|
||||||
|
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||||
|
blurToCancel={ false }
|
||||||
|
editable={ false }
|
||||||
|
initialValue={ alias } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
var canonical_alias_section;
|
||||||
|
if (can_set_canonical_alias) {
|
||||||
|
canonical_alias_section =
|
||||||
|
<select ref="canonical_alias" defaultValue={ canonical_alias }>
|
||||||
|
{ Object.keys(self.state.aliases).map(function(stateKey, i) {
|
||||||
|
return self.state.aliases[stateKey].map(function(alias, j) {
|
||||||
|
return <option value={ alias } key={ i + "_" + j }>{ alias }</option>
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
<option value="" key="unset">not specified</option>
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
canonical_alias_section = <b>{ canonical_alias || "not set" }</b>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliases_section =
|
||||||
|
<div>
|
||||||
|
<h3>Addresses</h3>
|
||||||
|
<div className="mx_RoomSettings_aliasLabel">The main address for this room is: { canonical_alias_section }</div>
|
||||||
|
<div className="mx_RoomSettings_aliasLabel">
|
||||||
|
{ this.state.aliases[domain].length
|
||||||
|
? "Local addresses for this room:"
|
||||||
|
: "This room has no local addresses" }
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_aliasesTable">
|
||||||
|
{ this.state.aliases[domain].map(function(alias, i) {
|
||||||
|
var deleteButton;
|
||||||
|
if (can_set_room_aliases) {
|
||||||
|
deleteButton = <img src="img/cancel-small.svg" width="14" height="14" alt="Delete"
|
||||||
|
onClick={ self.onAliasDeleted.bind(self, domain, i) }/>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomSettings_aliasesTableRow" key={ i }>
|
||||||
|
<EditableText
|
||||||
|
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||||
|
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||||
|
placeholder={ "New address (e.g. #foo:" + domain + ")" }
|
||||||
|
blurToCancel={ false }
|
||||||
|
onValueChanged={ self.onAliasChanged.bind(self, domain, i) }
|
||||||
|
editable={ can_set_room_aliases }
|
||||||
|
initialValue={ alias } />
|
||||||
|
<div className="mx_RoomSettings_deleteAlias">
|
||||||
|
{ deleteButton }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className="mx_RoomSettings_aliasesTableRow" key="new">
|
||||||
|
<EditableText
|
||||||
|
ref="add_alias"
|
||||||
|
className="mx_RoomSettings_alias mx_RoomSettings_editable"
|
||||||
|
placeholderClassName="mx_RoomSettings_aliasPlaceholder"
|
||||||
|
placeholder={ "New address (e.g. #foo:" + domain + ")" }
|
||||||
|
blurToCancel={ false }
|
||||||
|
onValueChanged={ self.onAliasAdded } />
|
||||||
|
<div className="mx_RoomSettings_addAlias">
|
||||||
|
<img src="img/plus.svg" width="14" height="14" alt="Add"
|
||||||
|
onClick={ self.onAliasAdded.bind(self, undefined) }/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ remote_aliases_section }
|
||||||
|
|
||||||
var change_avatar;
|
|
||||||
if (can_set_room_avatar) {
|
|
||||||
change_avatar = <div>
|
|
||||||
<h3>Room Icon</h3>
|
|
||||||
<ChangeAvatar room={this.props.room} />
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
var room_colors_section =
|
||||||
|
<div>
|
||||||
|
<h3>Room Colour</h3>
|
||||||
|
<div className="mx_RoomSettings_roomColors">
|
||||||
|
{room_colors.map(function(room_color, i) {
|
||||||
|
var selected;
|
||||||
|
if (i === self.state.color_scheme_index) {
|
||||||
|
selected =
|
||||||
|
<div className="mx_RoomSettings_roomColor_selected">
|
||||||
|
<img src="img/tick.svg" width="17" height="14" alt="./"/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
var boundClick = self.onColorSchemeChanged.bind(self, i)
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomSettings_roomColor"
|
||||||
|
key={ "room_color_" + i }
|
||||||
|
style={{ backgroundColor: room_color[1] }}
|
||||||
|
onClick={ boundClick }>
|
||||||
|
{ selected }
|
||||||
|
<div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
var user_levels_section;
|
||||||
|
if (Object.keys(user_levels).length) {
|
||||||
|
user_levels_section =
|
||||||
|
<div>
|
||||||
|
<h3>Privileged Users</h3>
|
||||||
|
<ul className="mx_RoomSettings_userLevels">
|
||||||
|
{Object.keys(user_levels).map(function(user, i) {
|
||||||
|
return (
|
||||||
|
<li className="mx_RoomSettings_userLevel" key={user}>
|
||||||
|
{ user } is a <PowerSelector value={ user_levels[user] } disabled={true}/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
user_levels_section = <div>No users have specific privileges in this room.</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
var banned = this.props.room.getMembersWithMembership("ban");
|
var banned = this.props.room.getMembersWithMembership("ban");
|
||||||
|
var banned_users_section;
|
||||||
|
if (banned.length) {
|
||||||
|
banned_users_section =
|
||||||
|
<div>
|
||||||
|
<h3>Banned users</h3>
|
||||||
|
<ul className="mx_RoomSettings_banned">
|
||||||
|
{banned.map(function(member, i) {
|
||||||
|
return (
|
||||||
|
<li key={i}>
|
||||||
|
{member.userId}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var create_event = this.props.room.currentState.getStateEvents('m.room.create', '');
|
||||||
|
var unfederatable_section;
|
||||||
|
if (create_event.getContent()["m.federate"] === false) {
|
||||||
|
unfederatable_section = <div className="mx_RoomSettings_powerLevel">Ths room is not accessible by remote Matrix servers.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: support editing custom events_levels
|
||||||
|
// TODO: support editing custom user_levels
|
||||||
|
|
||||||
|
var tags = [
|
||||||
|
{ name: "m.favourite", label: "Favourite", ref: "tag_favourite" },
|
||||||
|
{ name: "m.lowpriority", label: "Low priority", ref: "tag_lowpriority" },
|
||||||
|
];
|
||||||
|
|
||||||
|
Object.keys(this.state.tags).sort().forEach(function(tagName) {
|
||||||
|
if (tagName !== 'm.favourite' && tagName !== 'm.lowpriority') {
|
||||||
|
tags.push({ name: tagName, label: tagName });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var tags_section =
|
||||||
|
<div className="mx_RoomSettings_tags">
|
||||||
|
Tagged as:
|
||||||
|
{ can_set_tag ?
|
||||||
|
tags.map(function(tag, i) {
|
||||||
|
return (<label key={ i }>
|
||||||
|
<input type="checkbox"
|
||||||
|
ref={ tag.ref }
|
||||||
|
checked={ tag.name in self.state.tags }
|
||||||
|
onChange={ self.onTagChange.bind(self, tag.name) }/>
|
||||||
|
{ tag.label }
|
||||||
|
</label>);
|
||||||
|
}) : tags.map(function(tag) { return tag.label; }).join(", ")
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// FIXME: disable guests_read if the user hasn't turned on shared history
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSettings">
|
<div className="mx_RoomSettings">
|
||||||
<textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
|
|
||||||
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label> <br/>
|
|
||||||
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility == "shared"}/> Share message history with new users</label> <br/>
|
|
||||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
|
|
||||||
|
|
||||||
<h3>Power levels</h3>
|
{ tags_section }
|
||||||
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
|
|
||||||
<div>
|
<div className="mx_RoomSettings_toggles">
|
||||||
<label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
|
<label><input type="checkbox" ref="are_notifications_muted" defaultChecked={are_notifications_muted}/> Mute notifications for this room</label>
|
||||||
<input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
|
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label>
|
||||||
disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility === "shared" || history_visibility === "world_readable"}/> Share message history with new participants</label>
|
||||||
</div>
|
<label><input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/> Let guests join this room</label>
|
||||||
<div>
|
<label><input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/> Let users read message history without joining</label>
|
||||||
<label htmlFor="mx_RoomSettings_kick_level">Kick level</label>
|
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
|
||||||
<input type="text" defaultValue={kick_level} size="3" ref="kick" id="mx_RoomSettings_kick_level"
|
|
||||||
disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="mx_RoomSettings_redact_level">Redact level</label>
|
|
||||||
<input type="text" defaultValue={redact_level} size="3" ref="redact" id="mx_RoomSettings_redact_level"
|
|
||||||
disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="mx_RoomSettings_invite_level">Invite level</label>
|
|
||||||
<input type="text" defaultValue={invite_level} size="3" ref="invite" id="mx_RoomSettings_invite_level"
|
|
||||||
disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="mx_RoomSettings_event_level">Send event level</label>
|
|
||||||
<input type="text" defaultValue={send_level} size="3" ref="events_default" id="mx_RoomSettings_event_level"
|
|
||||||
disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="mx_RoomSettings_state_level">Set state level</label>
|
|
||||||
<input type="text" defaultValue={state_level} size="3" ref="state_default" id="mx_RoomSettings_state_level"
|
|
||||||
disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="mx_RoomSettings_user_level">Default user level</label>
|
|
||||||
<input type="text" defaultValue={default_user_level} size="3" ref="users_default"
|
|
||||||
id="mx_RoomSettings_user_level" disabled={!can_change_levels || current_user_level < default_user_level}
|
|
||||||
onChange={this.onPowerLevelsChanged}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>User levels</h3>
|
|
||||||
<div className="mx_RoomSettings_user_levels mx_RoomSettings_settings">
|
|
||||||
{Object.keys(user_levels).map(function(user, i) {
|
|
||||||
return (
|
|
||||||
<div key={user}>
|
|
||||||
<label htmlFor={"mx_RoomSettings_user_"+i}>{user}</label>
|
|
||||||
<input type="text" defaultValue={user_levels[user]} size="3" id={"mx_RoomSettings_user_"+i} disabled/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Event levels</h3>
|
{ room_colors_section }
|
||||||
<div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
|
|
||||||
|
{ aliases_section }
|
||||||
|
|
||||||
|
<h3>Permissions</h3>
|
||||||
|
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">The default role for new room members is </span>
|
||||||
|
<PowerSelector ref="users_default" value={default_user_level} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">To send messages, you must be a </span>
|
||||||
|
<PowerSelector ref="events_default" value={send_level} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">To invite users into the room, you must be a </span>
|
||||||
|
<PowerSelector ref="invite" value={invite_level} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">To configure the room, you must be a </span>
|
||||||
|
<PowerSelector ref="state_default" value={state_level} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">To kick users, you must be a </span>
|
||||||
|
<PowerSelector ref="kick" value={kick_level} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">To ban users, you must be a </span>
|
||||||
|
<PowerSelector ref="ban" value={ban_level} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomSettings_powerLevel">
|
||||||
|
<span className="mx_RoomSettings_powerLevelKey">To redact messages, you must be a </span>
|
||||||
|
<PowerSelector ref="redact" value={redact_level} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{Object.keys(events_levels).map(function(event_type, i) {
|
{Object.keys(events_levels).map(function(event_type, i) {
|
||||||
return (
|
return (
|
||||||
<div key={event_type}>
|
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||||
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
|
<span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span>
|
||||||
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
|
<PowerSelector value={ events_levels[event_type] } disabled={true} onChange={self.onPowerLevelsChanged}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{ unfederatable_section }
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Banned users</h3>
|
{ user_levels_section }
|
||||||
<div className="mx_RoomSettings_banned">
|
|
||||||
{banned.map(function(member, i) {
|
{ banned_users_section }
|
||||||
return (
|
|
||||||
<div key={i}>
|
<h3>Advanced</h3>
|
||||||
{member.userId}
|
<div className="mx_RoomSettings_settings">
|
||||||
</div>
|
This room's internal ID is <code>{ this.props.room.roomId }</code>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
{change_avatar}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ module.exports = React.createClass({
|
||||||
return connectDragSource(connectDropTarget(
|
return connectDragSource(connectDropTarget(
|
||||||
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||||
<div className="mx_RoomTile_avatar">
|
<div className="mx_RoomTile_avatar">
|
||||||
<RoomAvatar room={this.props.room} width="24" height="24" />
|
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||||
{ badge }
|
{ badge }
|
||||||
</div>
|
</div>
|
||||||
{ label }
|
{ label }
|
||||||
|
|
122
src/components/views/rooms/SearchableEntityList.js
Normal file
122
src/components/views/rooms/SearchableEntityList.js
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
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 React = require('react');
|
||||||
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
|
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||||
|
|
||||||
|
// A list capable of displaying entities which conform to the SearchableEntity
|
||||||
|
// interface which is an object containing getJsx(): Jsx and matches(query: string): boolean
|
||||||
|
var SearchableEntityList = React.createClass({
|
||||||
|
displayName: 'SearchableEntityList',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
searchPlaceholderText: React.PropTypes.string,
|
||||||
|
emptyQueryShowsAll: React.PropTypes.bool,
|
||||||
|
showInputBox: React.PropTypes.bool,
|
||||||
|
onQueryChanged: React.PropTypes.func, // fn(inputText)
|
||||||
|
onSubmit: React.PropTypes.func, // fn(inputText)
|
||||||
|
entities: React.PropTypes.array
|
||||||
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
showInputBox: true,
|
||||||
|
searchPlaceholderText: "Search",
|
||||||
|
entities: [],
|
||||||
|
emptyQueryShowsAll: false,
|
||||||
|
onSubmit: function() {},
|
||||||
|
onQueryChanged: function(input) {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
query: "",
|
||||||
|
results: this.getSearchResults("")
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
// pretend the query box was blanked out else filters could still be
|
||||||
|
// applied to other components which rely on onQueryChanged.
|
||||||
|
this.props.onQueryChanged("");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public-facing method to set the input query text to the given input.
|
||||||
|
* @param {string} input
|
||||||
|
*/
|
||||||
|
setQuery: function(input) {
|
||||||
|
this.setState({
|
||||||
|
query: input,
|
||||||
|
results: this.getSearchResults(input)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onQueryChanged: function(ev) {
|
||||||
|
var q = ev.target.value;
|
||||||
|
this.setState({
|
||||||
|
query: q,
|
||||||
|
results: this.getSearchResults(q)
|
||||||
|
});
|
||||||
|
this.props.onQueryChanged(q);
|
||||||
|
},
|
||||||
|
|
||||||
|
onQuerySubmit: function(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.props.onSubmit(this.state.query);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSearchResults: function(query) {
|
||||||
|
if (!query || query.length === 0) {
|
||||||
|
return this.props.emptyQueryShowsAll ? this.props.entities : []
|
||||||
|
}
|
||||||
|
return this.props.entities.filter(function(e) {
|
||||||
|
return e.matches(query);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var inputBox;
|
||||||
|
|
||||||
|
if (this.props.showInputBox) {
|
||||||
|
inputBox = (
|
||||||
|
<form onSubmit={this.onQuerySubmit}>
|
||||||
|
<input className="mx_SearchableEntityList_query" id="mx_SearchableEntityList_query" type="text"
|
||||||
|
onChange={this.onQueryChanged} value={this.state.query}
|
||||||
|
placeholder={this.props.searchPlaceholderText} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ "mx_SearchableEntityList " + (this.state.query.length ? "mx_SearchableEntityList_expanded" : "") }>
|
||||||
|
{inputBox}
|
||||||
|
<GeminiScrollbar autoshow={true} className="mx_SearchableEntityList_listWrapper">
|
||||||
|
<div className="mx_SearchableEntityList_list">
|
||||||
|
{this.state.results.map((entity) => {
|
||||||
|
return entity.getJsx();
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</GeminiScrollbar>
|
||||||
|
{ this.state.query.length ? <div className="mx_SearchableEntityList_hrWrapper"><hr/></div> : '' }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = SearchableEntityList;
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
var React = require('react');
|
var React = require('react');
|
||||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
|
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'TabCompleteBar',
|
displayName: 'TabCompleteBar',
|
||||||
|
@ -31,8 +32,9 @@ module.exports = React.createClass({
|
||||||
<div className="mx_TabCompleteBar">
|
<div className="mx_TabCompleteBar">
|
||||||
{this.props.entries.map(function(entry, i) {
|
{this.props.entries.map(function(entry, i) {
|
||||||
return (
|
return (
|
||||||
<div key={entry.getKey() || i + ""} className="mx_TabCompleteBar_item"
|
<div key={entry.getKey() || i + ""}
|
||||||
onClick={entry.onClick.bind(entry)} >
|
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||||
|
onClick={entry.onClick.bind(entry)} >
|
||||||
{entry.getImageJsx()}
|
{entry.getImageJsx()}
|
||||||
<span className="mx_TabCompleteBar_text">
|
<span className="mx_TabCompleteBar_text">
|
||||||
{entry.getText()}
|
{entry.getText()}
|
||||||
|
|
56
src/components/views/rooms/UserTile.js
Normal file
56
src/components/views/rooms/UserTile.js
Normal file
|
@ -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');
|
||||||
|
|
||||||
|
var Avatar = require("../../../Avatar");
|
||||||
|
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||||
|
var sdk = require('../../../index');
|
||||||
|
var dis = require('../../../dispatcher');
|
||||||
|
var Modal = require("../../../Modal");
|
||||||
|
|
||||||
|
module.exports = React.createClass({
|
||||||
|
displayName: 'UserTile',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
user: React.PropTypes.any.isRequired // User
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||||
|
var user = this.props.user;
|
||||||
|
var name = user.displayName || user.userId;
|
||||||
|
var active = -1;
|
||||||
|
|
||||||
|
// FIXME: make presence data update whenever User.presence changes...
|
||||||
|
active = (
|
||||||
|
(Date.now() - (user.lastPresenceTs - user.lastActiveAgo)) || -1
|
||||||
|
);
|
||||||
|
|
||||||
|
var BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||||
|
var avatarJsx = (
|
||||||
|
<BaseAvatar width={36} height={36} name={name} idName={user.userId}
|
||||||
|
url={ Avatar.avatarUrlForUser(user, 36, 36, "crop") } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EntityTile {...this.props} presenceState={user.presence} presenceActiveAgo={active}
|
||||||
|
name={name} title={user.userId} avatarJsx={avatarJsx} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -25,6 +25,8 @@ module.exports = React.createClass({
|
||||||
room: React.PropTypes.object,
|
room: React.PropTypes.object,
|
||||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||||
showUploadSection: React.PropTypes.bool,
|
showUploadSection: React.PropTypes.bool,
|
||||||
|
width: React.PropTypes.number,
|
||||||
|
height: React.PropTypes.number,
|
||||||
className: React.PropTypes.string
|
className: React.PropTypes.string
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -37,7 +39,9 @@ module.exports = React.createClass({
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
showUploadSection: true,
|
showUploadSection: true,
|
||||||
className: "mx_Dialog_content" // FIXME - shouldn't be this by default
|
className: "",
|
||||||
|
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
|
// Having just set an avatar we just display that since it will take a little
|
||||||
// time to propagate through to the RoomAvatar.
|
// time to propagate through to the RoomAvatar.
|
||||||
if (this.props.room && !this.avatarSet) {
|
if (this.props.room && !this.avatarSet) {
|
||||||
avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
|
avatarImg = <RoomAvatar room={this.props.room} width={ this.props.width } height={ this.props.height } resizeMethod='crop' />;
|
||||||
} else {
|
} else {
|
||||||
var style = {
|
var style = {
|
||||||
maxWidth: 320,
|
width: this.props.width,
|
||||||
maxHeight: 240,
|
height: this.props.height,
|
||||||
|
objectFit: 'cover',
|
||||||
};
|
};
|
||||||
avatarImg = <img src={this.state.avatarUrl} style={style} />;
|
avatarImg = <img className="mx_BaseAvatar_image" src={this.state.avatarUrl} style={style} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var uploadSection;
|
var uploadSection;
|
||||||
|
|
|
@ -99,7 +99,9 @@ module.exports = React.createClass({
|
||||||
var EditableText = sdk.getComponent('elements.EditableText');
|
var EditableText = sdk.getComponent('elements.EditableText');
|
||||||
return (
|
return (
|
||||||
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
|
<EditableText ref="displayname_edit" initialValue={this.state.displayName}
|
||||||
label="Click to set display name."
|
className="mx_EditableText"
|
||||||
|
placeholderClassName="mx_EditableText_placeholder"
|
||||||
|
placeholder="No display name"
|
||||||
onValueChanged={this.onValueChanged} />
|
onValueChanged={this.onValueChanged} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,12 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'CallView',
|
displayName: 'CallView',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// a callback which is called when the video within the callview
|
||||||
|
// due to a change in video metadata
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
|
@ -97,7 +103,7 @@ module.exports = React.createClass({
|
||||||
render: function(){
|
render: function(){
|
||||||
var VideoView = sdk.getComponent('voip.VideoView');
|
var VideoView = sdk.getComponent('voip.VideoView');
|
||||||
return (
|
return (
|
||||||
<VideoView ref="video" onClick={ this.props.onClick }/>
|
<VideoView ref="video" onClick={ this.props.onClick } onResize={ this.props.onResize }/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,9 +21,29 @@ var React = require('react');
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'VideoFeed',
|
displayName: 'VideoFeed',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
// a callback which is called when the video element is resized
|
||||||
|
// due to a change in video metadata
|
||||||
|
onResize: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.refs.vid.addEventListener('resize', this.onResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.refs.vid.removeEventListener('resize', this.onResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
onResize: function(e) {
|
||||||
|
if(this.props.onResize) {
|
||||||
|
this.props.onResize(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<video>
|
<video ref="vid">
|
||||||
</video>
|
</video>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -85,7 +85,7 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
||||||
<div className="mx_VideoView_remoteVideoFeed">
|
<div className="mx_VideoView_remoteVideoFeed">
|
||||||
<VideoFeed ref="remote"/>
|
<VideoFeed ref="remote" onResize={this.props.onResize}/>
|
||||||
<audio ref="remoteAudio"/>
|
<audio ref="remoteAudio"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_VideoView_localVideoFeed">
|
<div className="mx_VideoView_localVideoFeed">
|
||||||
|
|
23
src/email.js
Normal file
23
src/email.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
Copyright 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 EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
looksValid: function(email) {
|
||||||
|
return EMAIL_ADDRESS_REGEX.test(email);
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in a new issue