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
|
@ -35,7 +35,8 @@
|
|||
"react-dom": "^0.14.2",
|
||||
"react-gemini-scrollbar": "^2.0.1",
|
||||
"sanitize-html": "^1.11.1",
|
||||
"velocity-animate": "^1.2.3"
|
||||
"velocity-animate": "^1.2.3",
|
||||
"velocity-ui-pack": "^1.2.2"
|
||||
},
|
||||
"//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder",
|
||||
"//depsbuglink": "https://github.com/webpack/webpack/issues/1472",
|
||||
|
|
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';
|
||||
|
||||
var ContentRepo = require("matrix-js-sdk").ContentRepo;
|
||||
var MatrixClientPeg = require('./MatrixClientPeg');
|
||||
|
||||
module.exports = {
|
||||
|
@ -37,6 +37,17 @@ module.exports = {
|
|||
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) {
|
||||
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
|
||||
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
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var GuestAccess = require("./GuestAccess");
|
||||
|
||||
var matrixClient = null;
|
||||
|
||||
|
@ -33,7 +34,7 @@ function deviceId() {
|
|||
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 = {
|
||||
baseUrl: hs_url,
|
||||
idBaseUrl: is_url,
|
||||
|
@ -47,6 +48,15 @@ function createClient(hs_url, is_url, user_id, access_token) {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -54,12 +64,18 @@ if (localStorage) {
|
|||
var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org';
|
||||
var access_token = localStorage.getItem("mx_access_token");
|
||||
var user_id = localStorage.getItem("mx_user_id");
|
||||
var guestAccess = new GuestAccess(localStorage);
|
||||
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 {
|
||||
|
||||
constructor(guestAccess) {
|
||||
this.guestAccess = guestAccess;
|
||||
}
|
||||
|
||||
get() {
|
||||
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) {
|
||||
try {
|
||||
localStorage.clear();
|
||||
|
@ -105,7 +121,8 @@ class MatrixClient {
|
|||
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) {
|
||||
try {
|
||||
localStorage.setItem("mx_hs_url", hs_url);
|
||||
|
@ -122,6 +139,6 @@ class MatrixClient {
|
|||
}
|
||||
|
||||
if (!global.mxMatrixClient) {
|
||||
global.mxMatrixClient = new MatrixClient();
|
||||
global.mxMatrixClient = new MatrixClient(new GuestAccess(localStorage));
|
||||
}
|
||||
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;
|
||||
this.state = newState;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return; // don't try to set presence when a guest; it won't work.
|
||||
}
|
||||
|
||||
var self = this;
|
||||
MatrixClientPeg.get().setPresence(this.state).done(function() {
|
||||
console.log("Presence: %s", newState);
|
||||
|
|
|
@ -69,6 +69,10 @@ class Register extends Signup {
|
|||
this.params.idSid = idSid;
|
||||
}
|
||||
|
||||
setGuestAccessToken(token) {
|
||||
this.guestAccessToken = token;
|
||||
}
|
||||
|
||||
getStep() {
|
||||
return this._step;
|
||||
}
|
||||
|
@ -126,7 +130,8 @@ class Register extends Signup {
|
|||
}
|
||||
|
||||
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) {
|
||||
self.credentials = result;
|
||||
self.setStep("COMPLETE");
|
||||
|
@ -147,6 +152,8 @@ class Register extends Signup {
|
|||
} else {
|
||||
if (error.errcode === 'M_USER_IN_USE') {
|
||||
throw new Error("Username in use");
|
||||
} else if (error.errcode == 'M_INVALID_USERNAME') {
|
||||
throw new Error("User names may only contain alphanumeric characters, underscores or dots!");
|
||||
} else if (error.httpStatus == 401) {
|
||||
throw new Error("Authorisation failed!");
|
||||
} else if (error.httpStatus >= 400 && error.httpStatus < 500) {
|
||||
|
|
|
@ -18,6 +18,32 @@ var MatrixClientPeg = require("./MatrixClientPeg");
|
|||
var MatrixTools = require("./MatrixTools");
|
||||
var dis = require("./dispatcher");
|
||||
var encryption = require("./encryption");
|
||||
var Tinter = require("./Tinter");
|
||||
|
||||
|
||||
class Command {
|
||||
constructor(name, paramArgs, runFn) {
|
||||
this.name = name;
|
||||
this.paramArgs = paramArgs;
|
||||
this.runFn = runFn;
|
||||
}
|
||||
|
||||
getCommand() {
|
||||
return "/" + this.name;
|
||||
}
|
||||
|
||||
getCommandWithArgs() {
|
||||
return this.getCommand() + " " + this.paramArgs;
|
||||
}
|
||||
|
||||
run(roomId, args) {
|
||||
return this.runFn.bind(this)(roomId, args);
|
||||
}
|
||||
|
||||
getUsage() {
|
||||
return "Usage: " + this.getCommandWithArgs()
|
||||
}
|
||||
}
|
||||
|
||||
var reject = function(msg) {
|
||||
return {
|
||||
|
@ -33,16 +59,37 @@ var success = function(promise) {
|
|||
|
||||
var commands = {
|
||||
// Change your nickname
|
||||
nick: function(room_id, args) {
|
||||
nick: new Command("nick", "<display_name>", function(room_id, args) {
|
||||
if (args) {
|
||||
return success(
|
||||
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") {
|
||||
var client = MatrixClientPeg.get();
|
||||
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
|
||||
topic: function(room_id, args) {
|
||||
topic: new Command("topic", "<topic>", function(room_id, args) {
|
||||
if (args) {
|
||||
return success(
|
||||
MatrixClientPeg.get().setRoomTopic(room_id, args)
|
||||
);
|
||||
}
|
||||
return reject("Usage: /topic <topic>");
|
||||
},
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Invite a user
|
||||
invite: function(room_id, args) {
|
||||
invite: new Command("invite", "<userId>", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
|
@ -81,11 +128,11 @@ var commands = {
|
|||
);
|
||||
}
|
||||
}
|
||||
return reject("Usage: /invite <userId>");
|
||||
},
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Join a room
|
||||
join: function(room_id, args) {
|
||||
join: new Command("join", "<room_alias>", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
|
@ -94,8 +141,7 @@ var commands = {
|
|||
return reject("Usage: /join #alias:domain");
|
||||
}
|
||||
if (!room_alias.match(/:/)) {
|
||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
||||
room_alias += ':' + domain;
|
||||
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
||||
}
|
||||
|
||||
// Try to find a room with this alias
|
||||
|
@ -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;
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
var room_alias = matches[1];
|
||||
if (room_alias[0] !== '#') {
|
||||
return reject("Usage: /part [#alias:domain]");
|
||||
return reject(this.getUsage());
|
||||
}
|
||||
if (!room_alias.match(/:/)) {
|
||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
||||
room_alias += ':' + domain;
|
||||
room_alias += ':' + MatrixClientPeg.get().getDomain();
|
||||
}
|
||||
|
||||
// Try to find a room with this alias
|
||||
|
@ -175,10 +220,10 @@ var commands = {
|
|||
dis.dispatch({action: 'view_next_room'});
|
||||
})
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
// Kick a user from the room with an optional reason
|
||||
kick: function(room_id, args) {
|
||||
kick: new Command("kick", "<userId> [<reason>]", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
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: function(room_id, args) {
|
||||
ban: new Command("ban", "<userId> [<reason>]", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
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: function(room_id, args) {
|
||||
unban: new Command("unban", "<userId>", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
|
@ -214,11 +259,11 @@ var commands = {
|
|||
);
|
||||
}
|
||||
}
|
||||
return reject("Usage: /unban <userId>");
|
||||
},
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// 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) {
|
||||
var matches = args.match(/^(\S+?)( +(\d+))?$/);
|
||||
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
|
||||
deop: function(room_id, args) {
|
||||
deop: new Command("deop", "<userId>", function(room_id, args) {
|
||||
if (args) {
|
||||
var matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
|
@ -266,12 +311,14 @@ var commands = {
|
|||
);
|
||||
}
|
||||
}
|
||||
return reject("Usage: /deop <userId>");
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
})
|
||||
};
|
||||
|
||||
// helpful aliases
|
||||
commands.j = commands.join;
|
||||
var aliases = {
|
||||
j: "join"
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
|
@ -291,13 +338,26 @@ module.exports = {
|
|||
var cmd = bits[1].substring(1).toLowerCase();
|
||||
var args = bits[3];
|
||||
if (cmd === "me") return null;
|
||||
if (aliases[cmd]) {
|
||||
cmd = aliases[cmd];
|
||||
}
|
||||
if (commands[cmd]) {
|
||||
return commands[cmd](roomId, args);
|
||||
return commands[cmd].run(roomId, args);
|
||||
}
|
||||
else {
|
||||
return reject("Unrecognised command: " + input);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
||||
constructor(opts) {
|
||||
opts.startingWordSuffix = opts.startingWordSuffix || "";
|
||||
opts.wordSuffix = opts.wordSuffix || "";
|
||||
opts.allowLooping = opts.allowLooping || false;
|
||||
opts.autoEnterTabComplete = opts.autoEnterTabComplete || false;
|
||||
opts.onClickCompletes = opts.onClickCompletes || false;
|
||||
|
@ -58,7 +56,7 @@ class TabComplete {
|
|||
// assign onClick listeners for each entry to complete the text
|
||||
this.list.forEach((l) => {
|
||||
l.onClick = () => {
|
||||
this.completeTo(l.getText());
|
||||
this.completeTo(l);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -93,10 +91,12 @@ class TabComplete {
|
|||
|
||||
/**
|
||||
* Do an auto-complete with the given word. This terminates the tab-complete.
|
||||
* @param {string} someVal
|
||||
* @param {Entry} entry The tab-complete entry to complete to.
|
||||
*/
|
||||
completeTo(someVal) {
|
||||
this.textArea.value = this._replaceWith(someVal, true);
|
||||
completeTo(entry) {
|
||||
this.textArea.value = this._replaceWith(
|
||||
entry.getFillText(), true, entry.getSuffix(this.isFirstWord)
|
||||
);
|
||||
this.stopTabCompleting();
|
||||
// keep focus on the text area
|
||||
this.textArea.focus();
|
||||
|
@ -222,8 +222,9 @@ class TabComplete {
|
|||
if (!this.inPassiveMode) {
|
||||
// set textarea to this new value
|
||||
this.textArea.value = this._replaceWith(
|
||||
this.matchedList[this.currentIndex].text,
|
||||
this.currentIndex !== 0 // don't suffix the original text!
|
||||
this.matchedList[this.currentIndex].getFillText(),
|
||||
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 partial word. If we just use string.replace() with the regex it will
|
||||
// replace the partial word AND the character of whitespace. We want to
|
||||
|
@ -258,13 +259,12 @@ class TabComplete {
|
|||
boundaryChar = "";
|
||||
}
|
||||
|
||||
var replacementText = (
|
||||
boundaryChar + newVal + (
|
||||
includeSuffix ?
|
||||
(this.isFirstWord ? this.opts.startingWordSuffix : this.opts.wordSuffix) :
|
||||
""
|
||||
)
|
||||
);
|
||||
suffix = suffix || "";
|
||||
if (!includeSuffix) {
|
||||
suffix = "";
|
||||
}
|
||||
|
||||
var replacementText = boundaryChar + newVal + suffix;
|
||||
return this.originalText.replace(MATCH_REGEX, function() {
|
||||
return replacementText; // function form to avoid `$` special-casing
|
||||
});
|
||||
|
|
|
@ -28,6 +28,14 @@ class Entry {
|
|||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} The text to insert into the input box. Most of the time
|
||||
* this is the same as getText().
|
||||
*/
|
||||
getFillText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {ReactClass} Raw JSX
|
||||
*/
|
||||
|
@ -42,6 +50,14 @@ class Entry {
|
|||
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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
constructor(member) {
|
||||
super(member.name || member.userId);
|
||||
|
@ -66,6 +107,10 @@ class MemberEntry extends Entry {
|
|||
getKey() {
|
||||
return this.member.userId;
|
||||
}
|
||||
|
||||
getSuffix(isFirstWord) {
|
||||
return isFirstWord ? ": " : " ";
|
||||
}
|
||||
}
|
||||
|
||||
MemberEntry.fromMemberList = function(members) {
|
||||
|
@ -99,3 +144,4 @@ MemberEntry.fromMemberList = function(members) {
|
|||
|
||||
module.exports.Entry = Entry;
|
||||
module.exports.MemberEntry = MemberEntry;
|
||||
module.exports.CommandEntry = CommandEntry;
|
||||
|
|
|
@ -66,7 +66,7 @@ function textForMemberEvent(ev) {
|
|||
function textForTopicEvent(ev) {
|
||||
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) {
|
||||
|
|
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 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)
|
||||
|
@ -31,8 +32,14 @@ class UserActivity {
|
|||
start() {
|
||||
document.onmousemove = 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.lastDispatchAtTs = 0;
|
||||
this.activityEndTimer = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -41,10 +48,19 @@ class UserActivity {
|
|||
stop() {
|
||||
document.onmousemove = 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) {
|
||||
if (event.screenX) {
|
||||
if (event.screenX && event.type == "mousemove") {
|
||||
if (event.screenX === this.lastScreenX &&
|
||||
event.screenY === this.lastScreenY)
|
||||
{
|
||||
|
@ -55,12 +71,32 @@ class UserActivity {
|
|||
this.lastScreenY = event.screenY;
|
||||
}
|
||||
|
||||
this.lastActivityAtTs = (new Date).getTime();
|
||||
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL) {
|
||||
this.lastActivityAtTs = new Date().getTime();
|
||||
if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) {
|
||||
this.lastDispatchAtTs = this.lastActivityAtTs;
|
||||
dis.dispatch({
|
||||
action: 'user_activity'
|
||||
});
|
||||
if (!this.activityEndTimer) {
|
||||
this.activityEndTimer = setTimeout(
|
||||
this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_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';
|
||||
|
||||
var q = require("q");
|
||||
var MatrixClientPeg = require("./MatrixClientPeg");
|
||||
var Notifier = require("./Notifier");
|
||||
|
||||
|
@ -35,6 +35,11 @@ module.exports = {
|
|||
},
|
||||
|
||||
loadThreePids: function() {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
return q({
|
||||
threepids: []
|
||||
}); // guests can't poke 3pid endpoint
|
||||
}
|
||||
return MatrixClientPeg.get().getThreePids();
|
||||
},
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ module.exports = React.createClass({
|
|||
var old = oldChildren[c.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() {
|
||||
// special case visibility because it's nonsensical to animate an invisible element
|
||||
// so we always hidden->visible pre-transition and visible->hidden after
|
||||
|
@ -73,6 +73,7 @@ module.exports = React.createClass({
|
|||
|
||||
collectNode: function(k, node) {
|
||||
if (
|
||||
node &&
|
||||
this.nodes[k] === undefined &&
|
||||
node.props.startStyle &&
|
||||
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.UploadBar'] = require('./components/structures/UploadBar');
|
||||
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.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
|
||||
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.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
|
||||
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.PowerSelector'] = require('./components/views/elements/PowerSelector');
|
||||
module.exports.components['views.elements.ProgressBar'] = require('./components/views/elements/ProgressBar');
|
||||
module.exports.components['views.elements.TintableSvg'] = require('./components/views/elements/TintableSvg');
|
||||
module.exports.components['views.elements.UserSelector'] = require('./components/views/elements/UserSelector');
|
||||
module.exports.components['views.login.CaptchaForm'] = require('./components/views/login/CaptchaForm');
|
||||
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.TextualEvent'] = require('./components/views/messages/TextualEvent');
|
||||
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.MemberInfo'] = require('./components/views/rooms/MemberInfo');
|
||||
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
|
||||
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile');
|
||||
module.exports.components['views.rooms.MessageComposer'] = require('./components/views/rooms/MessageComposer');
|
||||
module.exports.components['views.rooms.PresenceLabel'] = require('./components/views/rooms/PresenceLabel');
|
||||
module.exports.components['views.rooms.RoomHeader'] = require('./components/views/rooms/RoomHeader');
|
||||
module.exports.components['views.rooms.RoomList'] = require('./components/views/rooms/RoomList');
|
||||
module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/views/rooms/RoomPreviewBar');
|
||||
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.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList');
|
||||
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.UserTile'] = require('./components/views/rooms/UserTile');
|
||||
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.ChangePassword'] = require('./components/views/settings/ChangePassword');
|
||||
|
|
|
@ -251,7 +251,7 @@ module.exports = React.createClass({
|
|||
var UserSelector = sdk.getComponent("elements.UserSelector");
|
||||
var RoomHeader = sdk.getComponent("rooms.RoomHeader");
|
||||
|
||||
var domain = MatrixClientPeg.get().credentials.userId.replace(/^.*:/, '');
|
||||
var domain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
return (
|
||||
<div className="mx_CreateRoom">
|
||||
|
|
|
@ -31,6 +31,7 @@ var Registration = require("./login/Registration");
|
|||
var PostRegistration = require("./login/PostRegistration");
|
||||
|
||||
var Modal = require("../../Modal");
|
||||
var Tinter = require("../../Tinter");
|
||||
var sdk = require('../../index');
|
||||
var MatrixTools = require('../../MatrixTools');
|
||||
var linkifyMatrix = require("../../linkify-matrix");
|
||||
|
@ -43,6 +44,7 @@ module.exports = React.createClass({
|
|||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
enableGuest: React.PropTypes.bool,
|
||||
startingQueryParams: React.PropTypes.object
|
||||
},
|
||||
|
||||
|
@ -63,7 +65,8 @@ module.exports = React.createClass({
|
|||
collapse_lhs: false,
|
||||
collapse_rhs: 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 (MatrixClientPeg.get().getRooms().length) {
|
||||
|
@ -88,8 +91,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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);
|
||||
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.focusComposer = false;
|
||||
|
@ -98,8 +114,11 @@ module.exports = React.createClass({
|
|||
this.scrollStateMap = {};
|
||||
document.addEventListener("keydown", this.onKeyDown);
|
||||
window.addEventListener("focus", this.onFocus);
|
||||
|
||||
if (this.state.logged_in) {
|
||||
this.notifyNewScreen('');
|
||||
} else if (this._autoRegisterAsGuest) {
|
||||
this._registerAsGuest();
|
||||
} else {
|
||||
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) {
|
||||
var roomIndexDelta = 1;
|
||||
|
||||
|
@ -185,6 +232,21 @@ module.exports = React.createClass({
|
|||
screen: 'post_registration'
|
||||
});
|
||||
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':
|
||||
if (this.state.logged_in) return;
|
||||
|
||||
|
@ -248,7 +310,10 @@ module.exports = React.createClass({
|
|||
});
|
||||
break;
|
||||
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;
|
||||
case 'view_prev_room':
|
||||
roomIndexDelta = -1;
|
||||
|
@ -301,8 +366,29 @@ module.exports = React.createClass({
|
|||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this._setPage(this.PageTypes.CreateRoom);
|
||||
this.notifyNewScreen('new');
|
||||
//this._setPage(this.PageTypes.CreateRoom);
|
||||
//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;
|
||||
case 'view_room_directory':
|
||||
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
|
||||
this._updateScrollMap();
|
||||
|
||||
|
@ -363,7 +449,16 @@ module.exports = React.createClass({
|
|||
if (room) {
|
||||
var theAlias = MatrixTools.getCanonicalAliasForRoom(room);
|
||||
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);
|
||||
newState.ready = true;
|
||||
}
|
||||
|
@ -372,6 +467,9 @@ module.exports = React.createClass({
|
|||
var scrollState = this.scrollStateMap[roomId];
|
||||
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
|
||||
|
@ -387,10 +485,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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(
|
||||
credentials.homeserverUrl, credentials.identityServerUrl,
|
||||
credentials.userId, credentials.accessToken
|
||||
credentials.userId, credentials.accessToken, credentials.guest
|
||||
);
|
||||
this.setState({
|
||||
screen: undefined,
|
||||
|
@ -457,7 +556,9 @@ module.exports = React.createClass({
|
|||
UserActivity.start();
|
||||
Presence.start();
|
||||
cli.startClient({
|
||||
pendingEventOrdering: "end"
|
||||
pendingEventOrdering: "end",
|
||||
// deliberately huge limit for now to avoid hitting gappy /sync's until gappy /sync performance improves
|
||||
initialSyncLimit: 250,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -511,6 +612,11 @@ module.exports = React.createClass({
|
|||
action: 'token_login',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'forgot_password') {
|
||||
dis.dispatch({
|
||||
action: 'start_password_recovery',
|
||||
params: params
|
||||
});
|
||||
} else if (screen == 'new') {
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
|
@ -566,6 +672,8 @@ module.exports = React.createClass({
|
|||
|
||||
onUserClick: function(event, userId) {
|
||||
event.preventDefault();
|
||||
|
||||
/*
|
||||
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||
var member = new Matrix.RoomMember(null, userId);
|
||||
ContextualMenu.createMenu(MemberInfo, {
|
||||
|
@ -573,6 +681,14 @@ module.exports = React.createClass({
|
|||
right: window.innerWidth - event.pageX,
|
||||
top: event.pageY
|
||||
});
|
||||
*/
|
||||
|
||||
var member = new Matrix.RoomMember(null, userId);
|
||||
if (!member) { return; }
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: member,
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutClick: function(event) {
|
||||
|
@ -620,10 +736,22 @@ module.exports = React.createClass({
|
|||
this.showScreen("login");
|
||||
},
|
||||
|
||||
onForgotPasswordClick: function() {
|
||||
this.showScreen("forgot_password");
|
||||
},
|
||||
|
||||
onRegistered: function(credentials) {
|
||||
this.onLoggedIn(credentials);
|
||||
// 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() {
|
||||
|
@ -639,7 +767,9 @@ module.exports = React.createClass({
|
|||
|
||||
var rooms = MatrixClientPeg.get().getRooms();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -671,6 +801,7 @@ module.exports = React.createClass({
|
|||
var CreateRoom = sdk.getComponent('structures.CreateRoom');
|
||||
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||
var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
|
||||
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
if (this.state.screen == 'post_registration') {
|
||||
|
@ -689,6 +820,7 @@ module.exports = React.createClass({
|
|||
<RoomView
|
||||
ref="roomView"
|
||||
roomId={this.state.currentRoom}
|
||||
autoPeek={this.state.autoPeek}
|
||||
key={this.state.currentRoom}
|
||||
ConferenceHandler={this.props.ConferenceHandler} />
|
||||
);
|
||||
|
@ -734,12 +866,20 @@ module.exports = React.createClass({
|
|||
</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 logoutLink;
|
||||
if (this.state.logged_in) {
|
||||
logoutLink = (
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
Logout
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>Logout</a>
|
||||
{logoutLink}
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.screen == 'register') {
|
||||
|
@ -749,19 +889,30 @@ module.exports = React.createClass({
|
|||
sessionId={this.state.register_session_id}
|
||||
idSid={this.state.register_id_sid}
|
||||
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}
|
||||
isUrl={this.props.config.default_is_url}
|
||||
registrationUrl={this.props.registrationUrl}
|
||||
onLoggedIn={this.onRegistered}
|
||||
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 {
|
||||
return (
|
||||
<Login
|
||||
onLoggedIn={this.onLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
homeserverUrl={this.props.config.default_hs_url}
|
||||
identityServerUrl={this.props.config.default_is_url} />
|
||||
identityServerUrl={this.props.config.default_is_url}
|
||||
onForgotPasswordClick={this.onForgotPasswordClick} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,11 +35,15 @@ var sdk = require('../../index');
|
|||
var CallHandler = require('../../CallHandler');
|
||||
var TabComplete = require("../../TabComplete");
|
||||
var MemberEntry = require("../../TabCompleteEntries").MemberEntry;
|
||||
var CommandEntry = require("../../TabCompleteEntries").CommandEntry;
|
||||
var Resend = require("../../Resend");
|
||||
var SlashCommands = require("../../SlashCommands");
|
||||
var dis = require("../../dispatcher");
|
||||
var Tinter = require("../../Tinter");
|
||||
|
||||
var PAGINATE_SIZE = 20;
|
||||
var INITIAL_SIZE = 20;
|
||||
var SEND_READ_RECEIPT_DELAY = 2000;
|
||||
|
||||
var DEBUG_SCROLL = false;
|
||||
|
||||
|
@ -53,7 +57,9 @@ if (DEBUG_SCROLL) {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'RoomView',
|
||||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
roomId: React.PropTypes.string,
|
||||
autoPeek: React.PropTypes.bool, // should we try to peek the room on mount, or has whoever invoked us already initiated a peek?
|
||||
},
|
||||
|
||||
/* properties in RoomView objects include:
|
||||
|
@ -74,13 +80,21 @@ module.exports = React.createClass({
|
|||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
hasUnsentMessages: this._hasUnsentMessages(room),
|
||||
callState: null,
|
||||
autoPeekDone: false, // track whether our autoPeek (if any) has completed)
|
||||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
readMarkerEventId: room ? room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId) : null,
|
||||
readMarkerGhostEventId: undefined
|
||||
}
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
MatrixClientPeg.get().on("Room", this.onNewRoom);
|
||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
|
||||
|
@ -88,8 +102,6 @@ module.exports = React.createClass({
|
|||
// xchat-style tab complete, add a colon if tab
|
||||
// completing at the start of the text
|
||||
this.tabComplete = new TabComplete({
|
||||
startingWordSuffix: ": ",
|
||||
wordSuffix: " ",
|
||||
allowLooping: false,
|
||||
autoEnterTabComplete: true,
|
||||
onClickCompletes: true,
|
||||
|
@ -97,24 +109,56 @@ module.exports = React.createClass({
|
|||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||
// - This is a room we cannot join at all. (no action can help us)
|
||||
// We can't try to /join because this may implicitly accept invites (!)
|
||||
// We can /peek though. If it fails then we present the join UI. If it
|
||||
// succeeds then great, show the preview (but we still may be able to /join!).
|
||||
if (!this.state.room) {
|
||||
if (this.props.autoPeek) {
|
||||
console.log("Attempting to peek into room %s", this.props.roomId);
|
||||
MatrixClientPeg.get().peekInRoom(this.props.roomId).catch((err) => {
|
||||
console.error("Failed to peek into room: %s", err);
|
||||
}).finally(() => {
|
||||
// we don't need to do anything - JS SDK will emit Room events
|
||||
// which will update the UI.
|
||||
this.setState({
|
||||
autoPeekDone: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._calculatePeekRules(this.state.room);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this.refs.messagePanel) {
|
||||
// disconnect the D&D event listeners from the message panel. This
|
||||
// is really just for hygiene - the messagePanel is going to be
|
||||
// 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;
|
||||
|
||||
if (this.refs.roomView) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
// is really just for hygiene - we're going to be
|
||||
// deleted anyway, so it doesn't matter if the event listeners
|
||||
// don't get cleaned up.
|
||||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
||||
messagePanel.removeEventListener('drop', this.onDrop);
|
||||
messagePanel.removeEventListener('dragover', this.onDragOver);
|
||||
messagePanel.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
messagePanel.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
roomView.removeEventListener('drop', this.onDrop);
|
||||
roomView.removeEventListener('dragover', this.onDragOver);
|
||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room", this.onNewRoom);
|
||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
|
||||
MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData);
|
||||
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
|
||||
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
|
||||
|
@ -122,6 +166,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
|
||||
Tinter.tint(); // reset colourscheme
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
|
@ -175,6 +221,12 @@ module.exports = React.createClass({
|
|||
|
||||
break;
|
||||
case 'user_activity':
|
||||
case 'user_activity_end':
|
||||
// we could treat user_activity_end differently and not
|
||||
// send receipts for messages that have arrived between
|
||||
// the actual user activity and the time they stopped
|
||||
// being active, but let's see if this is actually
|
||||
// necessary.
|
||||
this.sendReadReceipt();
|
||||
break;
|
||||
}
|
||||
|
@ -196,7 +248,7 @@ module.exports = React.createClass({
|
|||
},*/
|
||||
|
||||
onRoomTimeline: function(ev, room, toStartOfTimeline) {
|
||||
if (!this.isMounted()) return;
|
||||
if (this.unmounted) return;
|
||||
|
||||
// ignore anything that comes in whilst paginating: we get one
|
||||
// event for each new matrix event so this would cause a huge
|
||||
|
@ -227,6 +279,32 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onNewRoom: function(room) {
|
||||
if (room.roomId == this.props.roomId) {
|
||||
this.setState({
|
||||
room: room
|
||||
});
|
||||
}
|
||||
|
||||
this._calculatePeekRules(room);
|
||||
},
|
||||
|
||||
_calculatePeekRules: function(room) {
|
||||
var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
|
||||
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
|
||||
this.setState({
|
||||
guestsCanJoin: true
|
||||
});
|
||||
}
|
||||
|
||||
var historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
|
||||
if (historyVisibility && historyVisibility.getContent().history_visibility === "world_readable") {
|
||||
this.setState({
|
||||
canPeek: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onRoomName: function(room) {
|
||||
if (room.roomId == this.props.roomId) {
|
||||
this.setState({
|
||||
|
@ -235,9 +313,58 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
updateTint: function() {
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) return;
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
onRoomAccountData: function(room, event) {
|
||||
if (room.roomId == this.props.roomId) {
|
||||
if (event.getType === "org.matrix.room.color_scheme") {
|
||||
var color_scheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onRoomReceipt: function(receiptEvent, room) {
|
||||
if (room.roomId == this.props.roomId) {
|
||||
this.forceUpdate();
|
||||
var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId);
|
||||
var readMarkerGhostEventId = this.state.readMarkerGhostEventId;
|
||||
if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) {
|
||||
readMarkerGhostEventId = this.state.readMarkerEventId;
|
||||
}
|
||||
|
||||
|
||||
// if the event after the one referenced in the read receipt if sent by us, do nothing since
|
||||
// this is a temporary period before the synthesized receipt for our own message arrives
|
||||
var readMarkerGhostEventIndex;
|
||||
for (var i = 0; i < room.timeline.length; ++i) {
|
||||
if (room.timeline[i].getId() == readMarkerGhostEventId) {
|
||||
readMarkerGhostEventIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (readMarkerGhostEventIndex + 1 < room.timeline.length) {
|
||||
var nextEvent = room.timeline[readMarkerGhostEventIndex + 1];
|
||||
if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
readMarkerGhostEventId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
readMarkerEventId: readMarkerEventId,
|
||||
readMarkerGhostEventId: readMarkerGhostEventId,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -249,6 +376,14 @@ module.exports = React.createClass({
|
|||
if (member.roomId === this.props.roomId) {
|
||||
// a member state changed in this room, refresh the tab complete list
|
||||
this._updateTabCompleteList(this.state.room);
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var me = MatrixClientPeg.get().credentials.userId;
|
||||
if (this.state.joining && room.hasMembershipState(me, "join")) {
|
||||
this.setState({
|
||||
joining: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.props.ConferenceHandler) {
|
||||
|
@ -314,7 +449,24 @@ module.exports = React.createClass({
|
|||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
if (this.refs.roomView) {
|
||||
var roomView = ReactDOM.findDOMNode(this.refs.roomView);
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
}
|
||||
|
||||
this._updateTabCompleteList(this.state.room);
|
||||
|
||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||
// We use the setTimeout to avoid racing with focus_composer.
|
||||
if (this.state.room && this.state.room.getJoinedMembers().length == 1) {
|
||||
var inviteBox = document.getElementById("mx_SearchableEntityList_query");
|
||||
setTimeout(function() {
|
||||
inviteBox.focus();
|
||||
}, 50);
|
||||
}
|
||||
},
|
||||
|
||||
_updateTabCompleteList: function(room) {
|
||||
|
@ -322,7 +474,9 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
this.tabComplete.setCompletionList(
|
||||
MemberEntry.fromMemberList(room.getJoinedMembers())
|
||||
MemberEntry.fromMemberList(room.getJoinedMembers()).concat(
|
||||
CommandEntry.fromCommands(SlashCommands.getCommandList())
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -330,13 +484,10 @@ module.exports = React.createClass({
|
|||
var messagePanel = ReactDOM.findDOMNode(this.refs.messagePanel);
|
||||
this.refs.messagePanel.initialised = true;
|
||||
|
||||
messagePanel.addEventListener('drop', this.onDrop);
|
||||
messagePanel.addEventListener('dragover', this.onDragOver);
|
||||
messagePanel.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
messagePanel.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
|
||||
this.scrollToBottom();
|
||||
this.sendReadReceipt();
|
||||
|
||||
this.updateTint();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
@ -353,11 +504,14 @@ module.exports = React.createClass({
|
|||
_paginateCompleted: function() {
|
||||
debuglog("paginate complete");
|
||||
|
||||
this.setState({
|
||||
room: MatrixClientPeg.get().getRoom(this.props.roomId)
|
||||
});
|
||||
// we might have switched rooms since the paginate started - just bin
|
||||
// the results if so.
|
||||
if (this.unmounted) return;
|
||||
|
||||
this.setState({paginating: false});
|
||||
this.setState({
|
||||
room: MatrixClientPeg.get().getRoom(this.props.roomId),
|
||||
paginating: false,
|
||||
});
|
||||
},
|
||||
|
||||
onSearchResultsFillRequest: function(backwards) {
|
||||
|
@ -412,16 +566,29 @@ module.exports = React.createClass({
|
|||
|
||||
onJoinButtonClicked: function(ev) {
|
||||
var self = this;
|
||||
MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() {
|
||||
MatrixClientPeg.get().joinRoom(this.props.roomId).done(function() {
|
||||
// It is possible that there is no Room yet if state hasn't come down
|
||||
// from /sync - joinRoom will resolve when the HTTP request to join succeeds,
|
||||
// NOT when it comes down /sync. If there is no room, we'll keep the
|
||||
// joining flag set until we see it. Likewise, if our state is not
|
||||
// "join" we'll keep this flag set until it comes down /sync.
|
||||
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
|
||||
var me = MatrixClientPeg.get().credentials.userId;
|
||||
self.setState({
|
||||
joining: false,
|
||||
room: MatrixClientPeg.get().getRoom(self.props.roomId)
|
||||
joining: room ? !room.hasMembershipState(me, "join") : true,
|
||||
room: room
|
||||
});
|
||||
}, function(error) {
|
||||
self.setState({
|
||||
joining: false,
|
||||
joinError: error
|
||||
});
|
||||
var msg = error.message ? error.message : JSON.stringify(error);
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to join room",
|
||||
description: msg
|
||||
});
|
||||
});
|
||||
this.setState({
|
||||
joining: true
|
||||
|
@ -535,7 +702,7 @@ module.exports = React.createClass({
|
|||
|
||||
return searchPromise.then(function(results) {
|
||||
debuglog("search complete");
|
||||
if (!self.state.searching || self.searchId != localSearchId) {
|
||||
if (self.unmounted || !self.state.searching || self.searchId != localSearchId) {
|
||||
console.error("Discarding stale search results");
|
||||
return;
|
||||
}
|
||||
|
@ -553,7 +720,8 @@ module.exports = React.createClass({
|
|||
|
||||
// For overlapping highlights,
|
||||
// favour longer (more specific) terms first
|
||||
highlights = highlights.sort(function(a, b) { b.length - a.length });
|
||||
highlights = highlights.sort(function(a, b) {
|
||||
return b.length - a.length });
|
||||
|
||||
self.setState({
|
||||
searchHighlights: highlights,
|
||||
|
@ -648,9 +816,10 @@ module.exports = React.createClass({
|
|||
|
||||
var EventTile = sdk.getComponent('rooms.EventTile');
|
||||
|
||||
|
||||
var prevEvent = null; // the last event we showed
|
||||
var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap);
|
||||
var readMarkerIndex;
|
||||
var ghostIndex;
|
||||
for (var i = startIdx; i < this.state.room.timeline.length; i++) {
|
||||
var mxEv = this.state.room.timeline[i];
|
||||
|
||||
|
@ -664,6 +833,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
// now we've decided whether or not to show this message,
|
||||
// add the read up to marker if appropriate
|
||||
// doing this here means we implicitly do not show the marker
|
||||
// if it's at the bottom
|
||||
// NB. it would be better to decide where the read marker was going
|
||||
// when the state changed rather than here in the render method, but
|
||||
// this is where we decide what messages we show so it's the only
|
||||
// place we know whether we're at the bottom or not.
|
||||
var self = this;
|
||||
var mxEvSender = mxEv.sender ? mxEv.sender.userId : null;
|
||||
if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) {
|
||||
var hr;
|
||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
||||
self.readMarkerNode = n;
|
||||
}} />);
|
||||
readMarkerIndex = ret.length;
|
||||
ret.push(<li key="_readupto" className="mx_RoomView_myReadMarker_container">{hr}</li>);
|
||||
}
|
||||
|
||||
// is this a continuation of the previous message?
|
||||
var continuation = false;
|
||||
if (prevEvent !== null) {
|
||||
|
@ -700,13 +888,33 @@ module.exports = React.createClass({
|
|||
</li>
|
||||
);
|
||||
|
||||
// A read up to marker has died and returned as a ghost!
|
||||
// Lives in the dom as the ghost of the previous one while it fades away
|
||||
if (eventId == this.state.readMarkerGhostEventId) {
|
||||
ghostIndex = ret.length;
|
||||
}
|
||||
|
||||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
// splice the read marker ghost in now that we know whether the read receipt
|
||||
// is the last element or not, because we only decide as we're going along.
|
||||
if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) {
|
||||
var hr;
|
||||
hr = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
|
||||
Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
|
||||
self.setState({readMarkerGhostEventId: undefined});
|
||||
}});
|
||||
}} />);
|
||||
ret.splice(ghostIndex, 0, (
|
||||
<li key="_readuptoghost" className="mx_RoomView_myReadMarker_container">{hr}</li>
|
||||
));
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels) {
|
||||
uploadNewState: function(newVals) {
|
||||
var old_name = this.state.room.name;
|
||||
|
||||
var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', '');
|
||||
|
@ -730,56 +938,201 @@ module.exports = React.createClass({
|
|||
old_history_visibility = "shared";
|
||||
}
|
||||
|
||||
var old_guest_read = (old_history_visibility === "world_readable");
|
||||
|
||||
var old_guest_join = this.state.room.currentState.getStateEvents('m.room.guest_access', '');
|
||||
if (old_guest_join) {
|
||||
old_guest_join = (old_guest_join.getContent().guest_access === "can_join");
|
||||
}
|
||||
else {
|
||||
old_guest_join = false;
|
||||
}
|
||||
|
||||
var old_canonical_alias = this.state.room.currentState.getStateEvents('m.room.canonical_alias', '');
|
||||
if (old_canonical_alias) {
|
||||
old_canonical_alias = old_canonical_alias.getContent().alias;
|
||||
}
|
||||
else {
|
||||
old_canonical_alias = "";
|
||||
}
|
||||
|
||||
var deferreds = [];
|
||||
|
||||
if (old_name != new_name && new_name != undefined && new_name) {
|
||||
if (old_name != newVals.name && newVals.name != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name)
|
||||
MatrixClientPeg.get().setRoomName(this.state.room.roomId, newVals.name)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_topic != new_topic && new_topic != undefined) {
|
||||
if (old_topic != newVals.topic && newVals.topic != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic)
|
||||
MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, newVals.topic)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_join_rule != new_join_rule && new_join_rule != undefined) {
|
||||
if (old_join_rule != newVals.join_rule && newVals.join_rule != undefined) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.join_rules", {
|
||||
join_rule: new_join_rule,
|
||||
join_rule: newVals.join_rule,
|
||||
}, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) {
|
||||
deferreds.push(
|
||||
var visibilityDeferred;
|
||||
if (old_history_visibility != newVals.history_visibility &&
|
||||
newVals.history_visibility != undefined) {
|
||||
visibilityDeferred =
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.history_visibility", {
|
||||
history_visibility: new_history_visibility,
|
||||
history_visibility: newVals.history_visibility,
|
||||
}, ""
|
||||
);
|
||||
}
|
||||
|
||||
if (old_guest_read != newVals.guest_read ||
|
||||
old_guest_join != newVals.guest_join)
|
||||
{
|
||||
var guestDeferred =
|
||||
MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, {
|
||||
allowRead: newVals.guest_read,
|
||||
allowJoin: newVals.guest_join
|
||||
});
|
||||
|
||||
if (visibilityDeferred) {
|
||||
visibilityDeferred = visibilityDeferred.then(guestDeferred);
|
||||
}
|
||||
else {
|
||||
visibilityDeferred = guestDeferred;
|
||||
}
|
||||
}
|
||||
|
||||
if (visibilityDeferred) {
|
||||
deferreds.push(visibilityDeferred);
|
||||
}
|
||||
|
||||
// setRoomMutePushRule will do nothing if there is no change
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().setRoomMutePushRule(
|
||||
"global", this.state.room.roomId, newVals.are_notifications_muted
|
||||
)
|
||||
);
|
||||
|
||||
if (newVals.power_levels) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.power_levels", newVals.power_levels, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (newVals.alias_operations) {
|
||||
var oplist = [];
|
||||
for (var i = 0; i < newVals.alias_operations.length; i++) {
|
||||
var alias_operation = newVals.alias_operations[i];
|
||||
switch (alias_operation.type) {
|
||||
case 'put':
|
||||
oplist.push(
|
||||
MatrixClientPeg.get().createAlias(
|
||||
alias_operation.alias, this.state.room.roomId
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
oplist.push(
|
||||
MatrixClientPeg.get().deleteAlias(
|
||||
alias_operation.alias
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown alias operation, ignoring: " + alias_operation.type);
|
||||
}
|
||||
}
|
||||
|
||||
if (oplist.length) {
|
||||
var deferred = oplist[0];
|
||||
oplist.splice(1).forEach(function (f) {
|
||||
deferred = deferred.then(f);
|
||||
});
|
||||
deferreds.push(deferred);
|
||||
}
|
||||
}
|
||||
|
||||
if (newVals.tag_operations) {
|
||||
// FIXME: should probably be factored out with alias_operations above
|
||||
var oplist = [];
|
||||
for (var i = 0; i < newVals.tag_operations.length; i++) {
|
||||
var tag_operation = newVals.tag_operations[i];
|
||||
switch (tag_operation.type) {
|
||||
case 'put':
|
||||
oplist.push(
|
||||
MatrixClientPeg.get().setRoomTag(
|
||||
this.props.roomId, tag_operation.tag, {}
|
||||
)
|
||||
);
|
||||
break;
|
||||
case 'delete':
|
||||
oplist.push(
|
||||
MatrixClientPeg.get().deleteRoomTag(
|
||||
this.props.roomId, tag_operation.tag
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.log("Unknown tag operation, ignoring: " + tag_operation.type);
|
||||
}
|
||||
}
|
||||
|
||||
if (oplist.length) {
|
||||
var deferred = oplist[0];
|
||||
oplist.splice(1).forEach(function (f) {
|
||||
deferred = deferred.then(f);
|
||||
});
|
||||
deferreds.push(deferred);
|
||||
}
|
||||
}
|
||||
|
||||
if (old_canonical_alias !== newVals.canonical_alias) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.canonical_alias", {
|
||||
alias: newVals.canonical_alias
|
||||
}, ""
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (new_power_levels) {
|
||||
if (newVals.color_scheme) {
|
||||
deferreds.push(
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
this.state.room.roomId, "m.room.power_levels", new_power_levels, ""
|
||||
MatrixClientPeg.get().setRoomAccountData(
|
||||
this.state.room.roomId, "org.matrix.room.color_scheme", newVals.color_scheme
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (deferreds.length) {
|
||||
var self = this;
|
||||
q.all(deferreds).fail(function(err) {
|
||||
q.allSettled(deferreds).then(
|
||||
function(results) {
|
||||
var fails = results.filter(function(result) { return result.state !== "fulfilled" });
|
||||
if (fails.length) {
|
||||
fails.forEach(function(result) {
|
||||
console.error(result.reason);
|
||||
});
|
||||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Failed to set state",
|
||||
description: err.toString()
|
||||
description: fails.map(function(result) { return result.reason }).join("\n"),
|
||||
});
|
||||
self.refs.room_settings.resetState();
|
||||
}
|
||||
else {
|
||||
self.setState({
|
||||
editingRoomSettings: false
|
||||
});
|
||||
}
|
||||
}).finally(function() {
|
||||
self.setState({
|
||||
uploadingRoomSettings: false,
|
||||
|
@ -815,8 +1168,15 @@ module.exports = React.createClass({
|
|||
var lastReadEventIndex = this._getLastDisplayedEventIndexIgnoringOwn();
|
||||
if (lastReadEventIndex === null) return;
|
||||
|
||||
if (lastReadEventIndex > currentReadUpToEventIndex) {
|
||||
MatrixClientPeg.get().sendReadReceipt(this.state.room.timeline[lastReadEventIndex]);
|
||||
var lastReadEvent = this.state.room.timeline[lastReadEventIndex];
|
||||
|
||||
// we also remember the last read receipt we sent to avoid spamming the same one at the server repeatedly
|
||||
if (lastReadEventIndex > currentReadUpToEventIndex && this.last_rr_sent_event_id != lastReadEvent.getId()) {
|
||||
this.last_rr_sent_event_id = lastReadEvent.getId();
|
||||
MatrixClientPeg.get().sendReadReceipt(lastReadEvent).catch(() => {
|
||||
// it failed, so allow retries next time the user is active
|
||||
this.last_rr_sent_event_id = undefined;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -847,31 +1207,32 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onSettingsClick: function() {
|
||||
this.setState({editingRoomSettings: true});
|
||||
this.showSettings(true);
|
||||
},
|
||||
|
||||
onSaveClick: function() {
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
uploadingRoomSettings: true,
|
||||
});
|
||||
|
||||
var new_name = this.refs.header.getRoomName();
|
||||
var new_topic = this.refs.room_settings.getTopic();
|
||||
var new_join_rule = this.refs.room_settings.getJoinRules();
|
||||
var new_history_visibility = this.refs.room_settings.getHistoryVisibility();
|
||||
var new_power_levels = this.refs.room_settings.getPowerLevels();
|
||||
|
||||
this.uploadNewState(
|
||||
new_name,
|
||||
new_topic,
|
||||
new_join_rule,
|
||||
new_history_visibility,
|
||||
new_power_levels
|
||||
);
|
||||
this.uploadNewState({
|
||||
name: this.refs.header.getRoomName(),
|
||||
topic: this.refs.header.getTopic(),
|
||||
join_rule: this.refs.room_settings.getJoinRules(),
|
||||
history_visibility: this.refs.room_settings.getHistoryVisibility(),
|
||||
are_notifications_muted: this.refs.room_settings.areNotificationsMuted(),
|
||||
power_levels: this.refs.room_settings.getPowerLevels(),
|
||||
alias_operations: this.refs.room_settings.getAliasOperations(),
|
||||
tag_operations: this.refs.room_settings.getTagOperations(),
|
||||
canonical_alias: this.refs.room_settings.getCanonicalAlias(),
|
||||
guest_join: this.refs.room_settings.canGuestsJoin(),
|
||||
guest_read: this.refs.room_settings.canGuestsRead(),
|
||||
color_scheme: this.refs.room_settings.getColorScheme(),
|
||||
});
|
||||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
this.updateTint();
|
||||
this.setState({editingRoomSettings: false});
|
||||
},
|
||||
|
||||
|
@ -1019,20 +1380,32 @@ module.exports = React.createClass({
|
|||
// a minimum of the height of the video element, whilst also capping it from pushing out the page
|
||||
// so we have to do it via JS instead. In this implementation we cap the height by putting
|
||||
// a maxHeight on the underlying remote video tag.
|
||||
var auxPanelMaxHeight;
|
||||
if (this.refs.callView) {
|
||||
// XXX: don't understand why we have to call findDOMNode here in react 0.14 - it should already be a DOM node.
|
||||
var video = ReactDOM.findDOMNode(this.refs.callView.refs.video.refs.remote);
|
||||
|
||||
// header + footer + status + give us at least 100px of scrollback at all times.
|
||||
auxPanelMaxHeight = window.innerHeight - (83 + 72 + 36 + 100);
|
||||
// header + footer + status + give us at least 120px of scrollback at all times.
|
||||
var auxPanelMaxHeight = window.innerHeight -
|
||||
(83 + // height of RoomHeader
|
||||
36 + // height of the status area
|
||||
72 + // minimum height of the message compmoser
|
||||
(this.state.editingRoomSettings ? (window.innerHeight * 0.3) : 120)); // amount of desired scrollback
|
||||
|
||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||
// but it's better than the video going missing entirely
|
||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||
|
||||
if (this.refs.callView) {
|
||||
var video = this.refs.callView.getVideoView().getRemoteVideoElement();
|
||||
|
||||
video.style.maxHeight = auxPanelMaxHeight + "px";
|
||||
}
|
||||
|
||||
// we need to do this for general auxPanels too
|
||||
if (this.refs.auxPanel) {
|
||||
this.refs.auxPanel.style.maxHeight = auxPanelMaxHeight + "px";
|
||||
}
|
||||
|
||||
// the above might have made the aux panel resize itself, so now
|
||||
// we need to tell the gemini panel to adapt.
|
||||
this.onChildResize();
|
||||
},
|
||||
|
||||
onFullscreenClick: function() {
|
||||
|
@ -1066,6 +1439,24 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onChildResize: function() {
|
||||
// When the video or the message composer resizes, the scroll panel
|
||||
// also changes size. Work around GeminiScrollBar fail by telling it
|
||||
// about it. This also ensures that the scroll offset is updated.
|
||||
if (this.refs.messagePanel) {
|
||||
this.refs.messagePanel.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
// XXX: this is a bit naughty; we should be doing this via props
|
||||
if (show) {
|
||||
this.setState({editingRoomSettings: true});
|
||||
var self = this;
|
||||
setTimeout(function() { self.onResize() }, 0);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
|
||||
var MessageComposer = sdk.getComponent('rooms.MessageComposer');
|
||||
|
@ -1073,15 +1464,35 @@ module.exports = React.createClass({
|
|||
var RoomSettings = sdk.getComponent("rooms.RoomSettings");
|
||||
var SearchBar = sdk.getComponent("rooms.SearchBar");
|
||||
var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.props.roomId) {
|
||||
if (this.props.autoPeek && !this.state.autoPeekDone) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<div>
|
||||
<button onClick={this.onJoinButtonClicked}>Join Room</button>
|
||||
<div className="mx_RoomView">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header" room={this.state.room} simpleHeader="Join room"/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
canJoin={ true } canPreview={ false }/>
|
||||
<div className="error">{joinErrorText}</div>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
|
@ -1102,19 +1513,26 @@ module.exports = React.createClass({
|
|||
var inviteEvent = myMember.events.member;
|
||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
// XXX: Leaving this intentionally basic for now because invites are about to change totally
|
||||
// FIXME: This comment is now outdated - what do we need to fix? ^
|
||||
var joinErrorText = this.state.joinError ? "Failed to join room!" : "";
|
||||
var rejectErrorText = this.state.rejectError ? "Failed to reject invite!" : "";
|
||||
|
||||
// We deliberately don't try to peek into invites, even if we have permission to peek
|
||||
// as they could be a spam vector.
|
||||
// XXX: in future we could give the option of a 'Preview' button which lets them view anyway.
|
||||
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomHeader ref="header" room={this.state.room} simpleHeader="Room invite"/>
|
||||
<div className="mx_RoomView_invitePrompt">
|
||||
<div>{inviterName} has invited you to a room</div>
|
||||
<br/>
|
||||
<button ref="joinButton" onClick={this.onJoinButtonClicked}>Join</button>
|
||||
<button onClick={this.onRejectButtonClicked}>Reject</button>
|
||||
<RoomHeader ref="header" room={this.state.room}/>
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<RoomPreviewBar onJoinClick={ this.onJoinButtonClicked }
|
||||
onRejectClick={ this.onRejectButtonClicked }
|
||||
inviterName={ inviterName }
|
||||
canJoin={ true } canPreview={ false }/>
|
||||
<div className="error">{joinErrorText}</div>
|
||||
<div className="error">{rejectErrorText}</div>
|
||||
</div>
|
||||
<div className="mx_RoomView_messagePanel"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1147,7 +1565,7 @@ module.exports = React.createClass({
|
|||
if (this.state.syncState === "ERROR") {
|
||||
statusBar = (
|
||||
<div className="mx_RoomView_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" alt="/!\ "/>
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||
<div className="mx_RoomView_connectionLostBar_textArea">
|
||||
<div className="mx_RoomView_connectionLostBar_title">
|
||||
Connectivity to the server has been lost.
|
||||
|
@ -1166,8 +1584,8 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomView_tabCompleteImage">...</div>
|
||||
<div className="mx_RoomView_tabCompleteWrapper">
|
||||
<TabCompleteBar entries={this.tabComplete.peek(6)} />
|
||||
<div className="mx_RoomView_tabCompleteEol">
|
||||
<img src="img/eol.svg" width="22" height="16" alt="->|"/>
|
||||
<div className="mx_RoomView_tabCompleteEol" title="->|">
|
||||
<TintableSvg src="img/eol.svg" width="22" height="16"/>
|
||||
Auto-complete
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1177,7 +1595,7 @@ module.exports = React.createClass({
|
|||
else if (this.state.hasUnsentMessages) {
|
||||
statusBar = (
|
||||
<div className="mx_RoomView_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" alt="/!\ "/>
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ "/>
|
||||
<div className="mx_RoomView_connectionLostBar_textArea">
|
||||
<div className="mx_RoomView_connectionLostBar_title">
|
||||
Some of your messages have not been sent.
|
||||
|
@ -1214,7 +1632,7 @@ module.exports = React.createClass({
|
|||
|
||||
var aux = null;
|
||||
if (this.state.editingRoomSettings) {
|
||||
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} room={this.state.room} />;
|
||||
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} room={this.state.room} />;
|
||||
}
|
||||
else if (this.state.uploadingRoomSettings) {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -1223,6 +1641,18 @@ module.exports = React.createClass({
|
|||
else if (this.state.searching) {
|
||||
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress } onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch}/>;
|
||||
}
|
||||
else if (this.state.guestsCanJoin && MatrixClientPeg.get().isGuest() &&
|
||||
(!myMember || myMember.membership !== "join")) {
|
||||
aux = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true} />
|
||||
);
|
||||
}
|
||||
else if (this.state.canPeek &&
|
||||
(!myMember || myMember.membership !== "join")) {
|
||||
aux = (
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked} canJoin={true} />
|
||||
);
|
||||
}
|
||||
|
||||
var conferenceCallNotification = null;
|
||||
if (this.state.displayConfCallNotification) {
|
||||
|
@ -1240,9 +1670,9 @@ module.exports = React.createClass({
|
|||
var fileDropTarget = null;
|
||||
if (this.state.draggingFile) {
|
||||
fileDropTarget = <div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel">
|
||||
<img src="img/upload-big.svg" width="45" height="59" alt="Drop File Here"/><br/>
|
||||
Drop File Here
|
||||
<div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here">
|
||||
<TintableSvg src="img/upload-big.svg" width="45" height="59"/><br/>
|
||||
Drop file here to upload
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -1255,7 +1685,7 @@ module.exports = React.createClass({
|
|||
if (canSpeak) {
|
||||
messageComposer =
|
||||
<MessageComposer
|
||||
room={this.state.room} roomView={this} uploadFile={this.uploadFile}
|
||||
room={this.state.room} onResize={this.onChildResize} uploadFile={this.uploadFile}
|
||||
callState={this.state.callState} tabComplete={this.tabComplete} />
|
||||
}
|
||||
|
||||
|
@ -1278,25 +1708,29 @@ module.exports = React.createClass({
|
|||
|
||||
if (call.type === "video") {
|
||||
zoomButton = (
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick}>
|
||||
<img src="img/fullscreen.svg" title="Fill screen" alt="Fill screen" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title="Fill screen">
|
||||
<TintableSvg src="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
videoMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
|
||||
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"} width="31" height="27"/>
|
||||
<img src={call.isLocalVideoMuted() ? "img/video-unmute.svg" : "img/video-mute.svg"}
|
||||
alt={call.isLocalVideoMuted() ? "Click to unmute video" : "Click to mute video"}
|
||||
width="31" height="27"/>
|
||||
</div>
|
||||
}
|
||||
voiceMuteButton =
|
||||
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
|
||||
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"} width="21" height="26"/>
|
||||
<img src={call.isMicrophoneMuted() ? "img/voice-unmute.svg" : "img/voice-mute.svg"}
|
||||
alt={call.isMicrophoneMuted() ? "Click to unmute audio" : "Click to mute audio"}
|
||||
width="21" height="26"/>
|
||||
</div>
|
||||
|
||||
if (!statusBar) {
|
||||
statusBar =
|
||||
<div className="mx_RoomView_callBar">
|
||||
<img src="img/sound-indicator.svg" width="23" height="20" alt=""/>
|
||||
<img src="img/sound-indicator.svg" width="23" height="20"/>
|
||||
<b>Active call</b>
|
||||
</div>;
|
||||
}
|
||||
|
@ -1307,11 +1741,10 @@ module.exports = React.createClass({
|
|||
{ videoMuteButton }
|
||||
{ zoomButton }
|
||||
{ statusBar }
|
||||
<img className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
|
||||
<TintableSvg className="mx_RoomView_voipChevron" src="img/voip-chevron.svg" width="22" height="17"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
// if we have search results, we keep the messagepanel (so that it preserves its
|
||||
// scroll state), but hide it.
|
||||
var searchResultsPanel;
|
||||
|
@ -1339,7 +1772,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") }>
|
||||
<div className={ "mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "") } ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
editing={this.state.editingRoomSettings}
|
||||
onSearchClick={this.onSearchClick}
|
||||
|
@ -1352,9 +1785,10 @@ module.exports = React.createClass({
|
|||
onLeaveClick={
|
||||
(myMember && myMember.membership === "join") ? this.onLeaveClick : null
|
||||
} />
|
||||
<div className="mx_RoomView_auxPanel" ref="auxPanel">
|
||||
{ fileDropTarget }
|
||||
<div className="mx_RoomView_auxPanel">
|
||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}/>
|
||||
<CallView ref="callView" room={this.state.room} ConferenceHandler={this.props.ConferenceHandler}
|
||||
onResize={this.onChildResize} />
|
||||
{ conferenceCallNotification }
|
||||
{ aux }
|
||||
</div>
|
||||
|
|
|
@ -112,6 +112,14 @@ module.exports = React.createClass({
|
|||
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) {
|
||||
var sn = this._getScrollNode();
|
||||
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.
|
||||
checkFillState: function() {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sn = this._getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
|
@ -346,6 +358,12 @@ module.exports = React.createClass({
|
|||
* message panel.
|
||||
*/
|
||||
_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);
|
||||
|
||||
// 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) {
|
||||
upload = uploads[0];
|
||||
return <div />
|
||||
}
|
||||
|
||||
var innerProgressStyle = {
|
||||
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var sdk = require('../../index');
|
||||
var MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
var Modal = require('../../Modal');
|
||||
|
@ -21,6 +22,9 @@ var dis = require("../../dispatcher");
|
|||
var q = require('q');
|
||||
var version = require('../../../package.json').version;
|
||||
var UserSettingsStore = require('../../UserSettingsStore');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
var Email = require('../../email');
|
||||
var AddThreepid = require('../../AddThreepid');
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
|
@ -41,6 +45,7 @@ module.exports = React.createClass({
|
|||
threePids: [],
|
||||
clientVersion: version,
|
||||
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) {
|
||||
var self = this;
|
||||
var changeAvatar = this.refs.changeAvatar;
|
||||
|
@ -135,6 +146,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onUpgradeClicked: function() {
|
||||
dis.dispatch({
|
||||
action: "start_upgrade_registration"
|
||||
});
|
||||
},
|
||||
|
||||
onLogoutPromptCancel: function() {
|
||||
this.logoutModal.closeDialog();
|
||||
},
|
||||
|
@ -143,10 +160,81 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
var self = this;
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
switch (this.state.phase) {
|
||||
case "UserSettings.LOADING":
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
return (
|
||||
<Loader />
|
||||
);
|
||||
|
@ -160,14 +248,76 @@ module.exports = React.createClass({
|
|||
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
|
||||
var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
var Notifications = sdk.getComponent("settings.Notifications");
|
||||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
var avatarUrl = (
|
||||
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 (
|
||||
<div className="mx_UserSettings">
|
||||
<RoomHeader simpleHeader="Settings" />
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body" autoshow={true}>
|
||||
|
||||
<h2>Profile</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
|
@ -180,30 +330,19 @@ module.exports = React.createClass({
|
|||
<ChangeDisplayName />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
{threepidsSection}
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput">
|
||||
<img src="img/upload.svg"
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src="img/camera.svg"
|
||||
alt="Upload avatar" title="Upload avatar"
|
||||
width="19" height="24" />
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
||||
</div>
|
||||
|
@ -213,41 +352,18 @@ module.exports = React.createClass({
|
|||
<h2>Account</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<ChangePassword
|
||||
className="mx_UserSettings_accountTable"
|
||||
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}>
|
||||
<div className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
|
||||
Log out
|
||||
</div>
|
||||
|
||||
{accountJsx}
|
||||
</div>
|
||||
|
||||
<h2>Notifications</h2>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_notifTable">
|
||||
<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>
|
||||
<Notifications/>
|
||||
</div>
|
||||
|
||||
<h2>Advanced</h2>
|
||||
|
@ -260,6 +376,8 @@ module.exports = React.createClass({
|
|||
Version {this.state.clientVersion}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</GeminiScrollbar>
|
||||
</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,
|
||||
identityServerUrl: React.PropTypes.string,
|
||||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired
|
||||
onRegisterClick: React.PropTypes.func.isRequired,
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login',
|
|||
switch (step) {
|
||||
case 'm.login.password':
|
||||
return (
|
||||
<PasswordLogin onSubmit={this.onPasswordLogin} />
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick} />
|
||||
);
|
||||
case 'm.login.cas':
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,6 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
|
||||
var sdk = require('../../../index');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var dis = require('../../../dispatcher');
|
||||
var Signup = require("../../../Signup");
|
||||
var ServerConfig = require("../../views/login/ServerConfig");
|
||||
|
@ -40,6 +39,9 @@ module.exports = React.createClass({
|
|||
hsUrl: React.PropTypes.string,
|
||||
isUrl: 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.
|
||||
onLoginClick: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
@ -63,6 +65,7 @@ module.exports = React.createClass({
|
|||
this.registerLogic.setSessionId(this.props.sessionId);
|
||||
this.registerLogic.setRegistrationUrl(this.props.registrationUrl);
|
||||
this.registerLogic.setIdSid(this.props.idSid);
|
||||
this.registerLogic.setGuestAccessToken(this.props.guestAccessToken);
|
||||
this.registerLogic.recheckState();
|
||||
},
|
||||
|
||||
|
@ -156,6 +159,15 @@ module.exports = React.createClass({
|
|||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = `Password too short (min ${MIN_PASSWORD_LENGTH}).`;
|
||||
break;
|
||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = "This doesn't look like a valid email address";
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||
errMsg = "You need to enter a user name";
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown error code: %s", errCode);
|
||||
errMsg = "An unknown error occurred.";
|
||||
|
@ -186,7 +198,9 @@ module.exports = React.createClass({
|
|||
registerStep = (
|
||||
<RegistrationForm
|
||||
showEmail={true}
|
||||
defaultUsername={this.props.username}
|
||||
defaultEmail={this.props.email}
|
||||
disableUsernameChanges={this.props.disableUsernameChanges}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onError={this.onFormValidationFailed}
|
||||
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 Avatar = require('../../../Avatar');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require("../../../index");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
@ -27,7 +27,7 @@ module.exports = React.createClass({
|
|||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
resizeMethod: React.PropTypes.string
|
||||
},
|
||||
|
||||
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() {
|
||||
return {
|
||||
imageUrl: this._computeUrl()
|
||||
};
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
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() {
|
||||
// XXX: recalculates default avatar url constantly
|
||||
if (this.state.imageUrl === this.defaultAvatarUrl(this.props.member)) {
|
||||
var initial;
|
||||
if (this.props.member.name[0])
|
||||
initial = this.props.member.name[0].toUpperCase();
|
||||
if (initial === '@' && this.props.member.name[1])
|
||||
initial = this.props.member.name[1].toUpperCase();
|
||||
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
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 (
|
||||
<img className="mx_MemberAvatar mx_MemberAvatar_image" src={this.state.imageUrl}
|
||||
onError={this.onError}
|
||||
width={this.props.width} height={this.props.height}
|
||||
title={this.props.member.name}
|
||||
{...this.props}
|
||||
/>
|
||||
<BaseAvatar {...this.props} name={this.state.name} title={this.state.title}
|
||||
idName={this.props.member.userId} url={this.state.imageUrl} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,10 +16,18 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Avatar = require('../../../Avatar');
|
||||
var sdk = require("../../../index");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomAvatar',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 36,
|
||||
|
@ -29,84 +37,54 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
getInitialState: function() {
|
||||
this._update();
|
||||
return {
|
||||
imageUrl: this._nextUrl()
|
||||
urls: this.getImageUrls(this.props)
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.refreshImageUrl();
|
||||
},
|
||||
|
||||
refreshImageUrl: function(nextProps) {
|
||||
// If the list has changed, we start from scratch and re-check, but
|
||||
// don't do so unless the list has changed or we'd re-try fetching
|
||||
// images each time we re-rendered
|
||||
var newList = this.getUrlList();
|
||||
var differs = false;
|
||||
for (var i = 0; i < newList.length && i < this.urlList.length; ++i) {
|
||||
if (this.urlList[i] != newList[i]) differs = true;
|
||||
}
|
||||
if (this.urlList.length != newList.length) differs = true;
|
||||
|
||||
if (differs) {
|
||||
this._update();
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
this.setState({
|
||||
imageUrl: this._nextUrl()
|
||||
urls: this.getImageUrls(newProps)
|
||||
})
|
||||
},
|
||||
|
||||
getImageUrls: function(props) {
|
||||
return [
|
||||
this.getRoomAvatarUrl(props), // highest priority
|
||||
this.getOneToOneAvatar(props),
|
||||
this.getFallbackAvatar(props) // lowest priority
|
||||
].filter(function(url) {
|
||||
return url != null;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_update: function() {
|
||||
this.urlList = this.getUrlList();
|
||||
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(
|
||||
getRoomAvatarUrl: function(props) {
|
||||
return props.room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
this.props.width, this.props.height, this.props.resizeMethod,
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
);
|
||||
return url;
|
||||
},
|
||||
|
||||
// provided to the view class for convenience
|
||||
getOneToOneAvatar: function() {
|
||||
var userIds = Object.keys(this.props.room.currentState.members);
|
||||
getOneToOneAvatar: function(props) {
|
||||
var userIds = Object.keys(props.room.currentState.members);
|
||||
|
||||
if (userIds.length == 2) {
|
||||
var theOtherGuy = null;
|
||||
if (this.props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = this.props.room.currentState.members[userIds[1]];
|
||||
if (props.room.currentState.members[userIds[0]].userId == MatrixClientPeg.get().credentials.userId) {
|
||||
theOtherGuy = props.room.currentState.members[userIds[1]];
|
||||
} else {
|
||||
theOtherGuy = this.props.room.currentState.members[userIds[0]];
|
||||
theOtherGuy = props.room.currentState.members[userIds[0]];
|
||||
}
|
||||
return theOtherGuy.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
this.props.width, this.props.height, this.props.resizeMethod,
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} 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(),
|
||||
this.props.width, this.props.height, this.props.resizeMethod,
|
||||
props.width, props.height, props.resizeMethod,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
|
@ -114,58 +92,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
onError: function(ev) {
|
||||
this.setState({
|
||||
imageUrl: this._nextUrl()
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
|
||||
////////////
|
||||
|
||||
|
||||
getUrlList: function() {
|
||||
return [
|
||||
this.roomAvatarUrl(),
|
||||
this.getOneToOneAvatar(),
|
||||
this.getFallbackAvatar()
|
||||
];
|
||||
},
|
||||
|
||||
getFallbackAvatar: function() {
|
||||
return Avatar.defaultAvatarUrlForString(this.props.room.roomId);
|
||||
getFallbackAvatar: function(props) {
|
||||
return Avatar.defaultAvatarUrlForString(props.room.roomId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var style = {
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
var BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
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>
|
||||
<BaseAvatar {...this.props} name={this.props.room.name}
|
||||
idName={this.props.room.roomId} urls={this.state.urls} />
|
||||
);
|
||||
}
|
||||
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() {
|
||||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_ErrorDialogTitle">
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
|
|
|
@ -46,7 +46,7 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div className="mx_QuestionDialog">
|
||||
<div className="mx_QuestionDialogTitle">
|
||||
<div className="mx_Dialog_title">
|
||||
{this.props.title}
|
||||
</div>
|
||||
<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');
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
propTypes: {
|
||||
onValueChanged: React.PropTypes.func,
|
||||
initialValue: 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: {
|
||||
|
@ -36,38 +45,62 @@ module.exports = React.createClass({
|
|||
return {
|
||||
onValueChanged: function() {},
|
||||
initialValue: '',
|
||||
label: 'Click to set',
|
||||
label: '',
|
||||
placeholder: '',
|
||||
editable: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
value: this.props.initialValue,
|
||||
phase: this.Phases.Display,
|
||||
}
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState({
|
||||
value: nextProps.initialValue
|
||||
});
|
||||
if (nextProps.initialValue !== this.props.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() {
|
||||
return this.state.value;
|
||||
return this.value;
|
||||
},
|
||||
|
||||
setValue: function(val, shouldSubmit, suppressListener) {
|
||||
var self = this;
|
||||
this.setState({
|
||||
value: val,
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
if (!suppressListener) {
|
||||
self.onValueChanged(shouldSubmit);
|
||||
}
|
||||
});
|
||||
setValue: function(value) {
|
||||
this.value = value;
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
edit: function() {
|
||||
|
@ -80,65 +113,106 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
});
|
||||
this.value = this.props.initialValue;
|
||||
this.showPlaceholder(!this.value);
|
||||
this.onValueChanged(false);
|
||||
},
|
||||
|
||||
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) {
|
||||
// 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") {
|
||||
this.onFinish(ev);
|
||||
} else if (ev.key == "Escape") {
|
||||
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({
|
||||
phase: this.Phases.Edit,
|
||||
})
|
||||
},
|
||||
|
||||
onFocus: function(ev) {
|
||||
ev.target.setSelectionRange(0, ev.target.value.length);
|
||||
},
|
||||
//ev.target.setSelectionRange(0, ev.target.textContent.length);
|
||||
|
||||
onFinish: function(ev) {
|
||||
if (ev.target.value) {
|
||||
this.setValue(ev.target.value, ev.key === "Enter");
|
||||
} else {
|
||||
this.cancelEdit();
|
||||
var node = ev.target.childNodes[0];
|
||||
if (node) {
|
||||
var range = document.createRange();
|
||||
range.setStart(node, 0);
|
||||
range.setEnd(node, node.length);
|
||||
|
||||
var sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
},
|
||||
|
||||
onBlur: function() {
|
||||
onFinish: function(ev) {
|
||||
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() {
|
||||
var editable_el;
|
||||
|
||||
if (this.state.phase == this.Phases.Display) {
|
||||
if (this.state.value) {
|
||||
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.state.value}</div>;
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||
} else {
|
||||
editable_el = <div ref="display_div" onClick={this.onClickDiv}>{this.props.label}</div>;
|
||||
}
|
||||
} else if (this.state.phase == this.Phases.Edit) {
|
||||
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>
|
||||
);
|
||||
// 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}
|
||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_EditableText">
|
||||
{editable_el}
|
||||
</div>
|
||||
);
|
||||
return editable_el;
|
||||
}
|
||||
});
|
||||
|
|
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() {
|
||||
return (
|
||||
<div className="mx_ErrorDialog">
|
||||
<div className="mx_ErrorDialogTitle">
|
||||
<div className="mx_Dialog_title">
|
||||
Custom Server Options
|
||||
</div>
|
||||
<div className="mx_Dialog_content">
|
||||
|
|
|
@ -22,7 +22,8 @@ var ReactDOM = require('react-dom');
|
|||
*/
|
||||
module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||
propTypes: {
|
||||
onSubmit: React.PropTypes.func.isRequired // fn(username, password)
|
||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||
onForgotPasswordClick: React.PropTypes.func // fn()
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var forgotPasswordJsx;
|
||||
|
||||
if (this.props.onForgotPasswordClick) {
|
||||
forgotPasswordJsx = (
|
||||
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
|
||||
Forgot your password?
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.onSubmitForm}>
|
||||
|
@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
|||
value={this.state.password} onChange={this.onPasswordChanged}
|
||||
placeholder="Password" />
|
||||
<br />
|
||||
{forgotPasswordJsx}
|
||||
<input className="mx_Login_submit" type="submit" value="Log in" />
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -17,7 +17,15 @@ limitations under the License.
|
|||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var Velocity = require('velocity-animate');
|
||||
require('velocity-ui-pack');
|
||||
var sdk = require('../../../index');
|
||||
var 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.
|
||||
|
@ -30,6 +38,7 @@ module.exports = React.createClass({
|
|||
defaultUsername: React.PropTypes.string,
|
||||
showEmail: React.PropTypes.bool,
|
||||
minPasswordLength: React.PropTypes.number,
|
||||
disableUsernameChanges: React.PropTypes.bool,
|
||||
onError: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
|
||||
},
|
||||
|
@ -49,34 +58,28 @@ module.exports = React.createClass({
|
|||
email: this.props.defaultEmail,
|
||||
username: this.props.defaultUsername,
|
||||
password: null,
|
||||
passwordConfirm: null
|
||||
passwordConfirm: null,
|
||||
fieldValid: {}
|
||||
};
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
var pwd1 = this.refs.password.value.trim();
|
||||
var pwd2 = this.refs.passwordConfirm.value.trim()
|
||||
|
||||
var errCode;
|
||||
if (!pwd1 || !pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISSING";
|
||||
}
|
||||
else if (pwd1 !== pwd2) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_MISMATCH";
|
||||
}
|
||||
else if (pwd1.length < this.props.minPasswordLength) {
|
||||
errCode = "RegistrationForm.ERR_PASSWORD_LENGTH";
|
||||
}
|
||||
if (errCode) {
|
||||
this.props.onError(errCode);
|
||||
return;
|
||||
}
|
||||
// validate everything, in reverse order so
|
||||
// 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);
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
var promise = this.props.onRegisterClick({
|
||||
username: this.refs.username.value.trim(),
|
||||
password: pwd1,
|
||||
password: this.refs.password.value.trim(),
|
||||
email: this.refs.email.value.trim()
|
||||
});
|
||||
|
||||
|
@ -86,15 +89,120 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
var self = this;
|
||||
var emailSection, registerButton;
|
||||
if (this.props.showEmail) {
|
||||
emailSection = (
|
||||
<input className="mx_Login_field" type="text" ref="email"
|
||||
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) {
|
||||
|
@ -109,13 +217,20 @@ module.exports = React.createClass({
|
|||
{emailSection}
|
||||
<br />
|
||||
<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 />
|
||||
<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} />
|
||||
<br />
|
||||
<input className="mx_Login_field" type="password" ref="passwordConfirm"
|
||||
placeholder="Confirm password"
|
||||
style={this._styleField(FIELD_PASSWORD_CONFIRM)}
|
||||
onBlur={function() {self.validateField(FIELD_PASSWORD_CONFIRM)}}
|
||||
defaultValue={this.state.passwordConfirm} />
|
||||
<br />
|
||||
{registerButton}
|
||||
|
|
|
@ -19,6 +19,8 @@ limitations under the License.
|
|||
var React = require('react');
|
||||
var filesize = require('filesize');
|
||||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MFileBody',
|
||||
|
@ -52,12 +54,14 @@ module.exports = React.createClass({
|
|||
var httpUrl = cli.mxcUrlToHttp(content.url);
|
||||
var text = this.presentableTextForFile(content);
|
||||
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
if (httpUrl) {
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MImageBody_download">
|
||||
<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}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,7 @@ var filesize = require('filesize');
|
|||
var MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Modal = require('../../../Modal');
|
||||
var sdk = require('../../../index');
|
||||
var dis = require("../../../dispatcher");
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MImageBody',
|
||||
|
@ -97,6 +98,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
var content = this.props.mxEvent.getContent();
|
||||
var cli = MatrixClientPeg.get();
|
||||
|
||||
|
@ -118,7 +120,7 @@ module.exports = React.createClass({
|
|||
</a>
|
||||
<div className="mx_MImageBody_download">
|
||||
<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" })
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -36,6 +36,9 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
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")
|
||||
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>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -66,7 +66,8 @@ module.exports = React.createClass({
|
|||
title: "Kick error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -74,7 +75,8 @@ module.exports = React.createClass({
|
|||
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
var roomId = this.props.member.roomId;
|
||||
var target = this.props.member.userId;
|
||||
MatrixClientPeg.get().ban(roomId, target).done(function() {
|
||||
MatrixClientPeg.get().ban(roomId, target).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("Ban success");
|
||||
|
@ -83,7 +85,8 @@ module.exports = React.createClass({
|
|||
title: "Ban error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -127,7 +130,8 @@ module.exports = React.createClass({
|
|||
title: "Mute error",
|
||||
description: err.message
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
|
@ -154,6 +158,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
var defaultLevel = powerLevelEvent.getContent().users_default;
|
||||
var modLevel = me.powerLevel - 1;
|
||||
if (modLevel > 50 && defaultLevel < 50) modLevel = 50; // try to stick with the vector level defaults
|
||||
// toggle the level
|
||||
var newLevel = this.state.isTargetMod ? defaultLevel : modLevel;
|
||||
MatrixClientPeg.get().setPowerLevel(roomId, target, newLevel, powerLevelEvent).done(
|
||||
|
@ -166,7 +171,39 @@ module.exports = React.createClass({
|
|||
title: "Mod error",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
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();
|
||||
},
|
||||
|
||||
|
@ -209,7 +246,8 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().createRoom({
|
||||
invite: [this.props.member.userId],
|
||||
preset: "private_chat"
|
||||
}).done(function(res) {
|
||||
}).done(
|
||||
function(res) {
|
||||
self.setState({ creatingRoom: false });
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
|
@ -222,7 +260,8 @@ module.exports = React.createClass({
|
|||
"Failed to create room: %s", JSON.stringify(err)
|
||||
);
|
||||
self.props.onFinished();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -291,9 +330,15 @@ module.exports = React.createClass({
|
|||
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
|
||||
powerLevels.state_default
|
||||
);
|
||||
var levelToSend = (
|
||||
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
|
||||
powerLevels.events_default
|
||||
);
|
||||
|
||||
can.kick = me.powerLevel >= powerLevels.kick;
|
||||
can.ban = me.powerLevel >= powerLevels.ban;
|
||||
can.mute = me.powerLevel >= editPowerLevel;
|
||||
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend;
|
||||
can.modifyLevel = me.powerLevel > them.powerLevel;
|
||||
return can;
|
||||
},
|
||||
|
@ -317,12 +362,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
|
||||
interactButton = <div className="mx_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
|
||||
}
|
||||
else {
|
||||
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
|
||||
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
|
||||
if (this.props.member.userId !== MatrixClientPeg.get().credentials.userId) {
|
||||
// FIXME: we're referring to a vector component from react-sdk
|
||||
var BottomLeftMenuTile = sdk.getComponent('rooms.BottomLeftMenuTile');
|
||||
startChat = <BottomLeftMenuTile collapsed={ false } img="img/create-big.svg" label="Start chat" onClick={ this.onChatClick }/>
|
||||
}
|
||||
|
||||
if (this.state.creatingRoom) {
|
||||
|
@ -346,36 +390,57 @@ module.exports = React.createClass({
|
|||
{muteLabel}
|
||||
</div>;
|
||||
}
|
||||
if (this.state.can.modifyLevel) {
|
||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
|
||||
if (this.state.can.toggleMod) {
|
||||
var giveOpLabel = this.state.isTargetMod ? "Revoke Moderator" : "Make Moderator";
|
||||
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</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 PowerSelector = sdk.getComponent('elements.PowerSelector');
|
||||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<img className="mx_MemberInfo_cancel" src="img/cancel.svg" width="18" height="18" onClick={this.onCancel}/>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
||||
<h2>{ this.props.member.name }</h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.props.member.userId }
|
||||
</div>
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
power: { this.props.member.powerLevelNorm }%
|
||||
Level: <b><PowerSelector value={ parseInt(this.props.member.powerLevel) } disabled={ !this.state.can.modifyLevel } onChange={ this.onPowerChange }/></b>
|
||||
</div>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{interactButton}
|
||||
{muteButton}
|
||||
{kickButton}
|
||||
{banButton}
|
||||
{giveModButton}
|
||||
</div>
|
||||
|
||||
{ startChat }
|
||||
|
||||
{ adminTools }
|
||||
|
||||
{ spinner }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -15,12 +15,20 @@ limitations under the License.
|
|||
*/
|
||||
var React = require('react');
|
||||
var classNames = require('classnames');
|
||||
var Matrix = require("matrix-js-sdk");
|
||||
var q = require('q');
|
||||
var MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var Modal = require("../../../Modal");
|
||||
var Entities = require("../../../Entities");
|
||||
var sdk = require('../../../index');
|
||||
var GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
|
||||
var INITIAL_LOAD_NUM_MEMBERS = 50;
|
||||
var SHARE_HISTORY_WARNING = "Newly invited users will see the history of this room. "+
|
||||
"If you'd prefer invited users not to see messages that were sent before they joined, "+
|
||||
"turn off, 'Share message history with new users' in the settings for this room.";
|
||||
|
||||
var shown_invite_warning_this_session = false;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MemberList',
|
||||
|
@ -63,8 +71,13 @@ module.exports = React.createClass({
|
|||
self.setState({
|
||||
members: self.roomMembers()
|
||||
});
|
||||
// lazy load to prevent it blocking the first render
|
||||
self._loadUserList();
|
||||
}, 50);
|
||||
|
||||
|
||||
setTimeout
|
||||
|
||||
// Attach a SINGLE listener for global presence changes then locate the
|
||||
// member tile and re-render it. This is more efficient than every tile
|
||||
// evar attaching their own listener.
|
||||
|
@ -88,6 +101,21 @@ module.exports = React.createClass({
|
|||
/*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) {
|
||||
if (room.roomId !== this.props.roomId) {
|
||||
return;
|
||||
|
@ -96,6 +124,7 @@ module.exports = React.createClass({
|
|||
// we need to wait till the room is fully populated with state
|
||||
// before refreshing the member list else we get a stale list.
|
||||
this._updateList();
|
||||
this._loadUserList();
|
||||
},
|
||||
|
||||
onRoomStateMember: function(ev, state, member) {
|
||||
|
@ -131,12 +160,41 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
var promise;
|
||||
var invite_defer = q.defer();
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
var history_visibility = room.currentState.getStateEvents('m.room.history_visibility', '');
|
||||
if (history_visibility) history_visibility = history_visibility.getContent().history_visibility;
|
||||
|
||||
if (history_visibility == 'shared' && !shown_invite_warning_this_session) {
|
||||
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: "Warning",
|
||||
description: SHARE_HISTORY_WARNING,
|
||||
button: "Invite",
|
||||
onFinished: function(should_invite) {
|
||||
if (should_invite) {
|
||||
shown_invite_warning_this_session = true;
|
||||
invite_defer.resolve();
|
||||
} else {
|
||||
invite_defer.reject(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
invite_defer.resolve();
|
||||
}
|
||||
|
||||
var promise = invite_defer.promise;;
|
||||
if (isEmailAddress) {
|
||||
promise = MatrixClientPeg.get().inviteByEmail(this.props.roomId, inputText);
|
||||
promise = promise.then(function() {
|
||||
MatrixClientPeg.get().inviteByEmail(self.props.roomId, inputText);
|
||||
});
|
||||
}
|
||||
else {
|
||||
promise = MatrixClientPeg.get().invite(this.props.roomId, inputText);
|
||||
promise = promise.then(function() {
|
||||
MatrixClientPeg.get().invite(self.props.roomId, inputText);
|
||||
});
|
||||
}
|
||||
|
||||
self.setState({
|
||||
|
@ -151,11 +209,13 @@ module.exports = React.createClass({
|
|||
inviting: false
|
||||
});
|
||||
}, function(err) {
|
||||
if (err !== null) {
|
||||
console.error("Failed to invite: %s", JSON.stringify(err));
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: "Server error whilst inviting",
|
||||
description: err.message
|
||||
});
|
||||
}
|
||||
self.setState({
|
||||
inviting: false
|
||||
});
|
||||
|
@ -225,12 +285,23 @@ module.exports = React.createClass({
|
|||
return latB - latA;
|
||||
},
|
||||
|
||||
makeMemberTiles: function(membership) {
|
||||
onSearchQueryChanged: function(input) {
|
||||
this.setState({
|
||||
searchQuery: input
|
||||
});
|
||||
},
|
||||
|
||||
makeMemberTiles: function(membership, query) {
|
||||
var MemberTile = sdk.getComponent("rooms.MemberTile");
|
||||
query = (query || "").toLowerCase();
|
||||
|
||||
var self = this;
|
||||
return self.state.members.filter(function(userId) {
|
||||
|
||||
var memberList = self.state.members.filter(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
if (query && m.name.toLowerCase().indexOf(query) !== 0) {
|
||||
return false;
|
||||
}
|
||||
return m.membership == membership;
|
||||
}).map(function(userId) {
|
||||
var m = self.memberDict[userId];
|
||||
|
@ -238,11 +309,32 @@ module.exports = React.createClass({
|
|||
<MemberTile key={userId} member={m} ref={userId} />
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
onPopulateInvite: function(e) {
|
||||
this.onInvite(this.refs.invite.value);
|
||||
e.preventDefault();
|
||||
if (membership === "invite") {
|
||||
// include 3pid invites (m.room.third_party_invite) state events.
|
||||
// The HS may have already converted these into m.room.member invites so
|
||||
// we shouldn't add them if the 3pid invite state key (token) is in the
|
||||
// member invite (content.third_party_invite.signed.token)
|
||||
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
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() {
|
||||
|
@ -252,22 +344,25 @@ module.exports = React.createClass({
|
|||
<Loader />
|
||||
);
|
||||
} else {
|
||||
var SearchableEntityList = sdk.getComponent("rooms.SearchableEntityList");
|
||||
|
||||
return (
|
||||
<form onSubmit={this.onPopulateInvite}>
|
||||
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite user (email)"/>
|
||||
</form>
|
||||
<SearchableEntityList searchPlaceholderText={"Invite / Search"}
|
||||
onSubmit={this.onInvite}
|
||||
onQueryChanged={this.onSearchQueryChanged}
|
||||
entities={Entities.fromUsers(this.userList || [], true, this.onInvite)} />
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var invitedSection = null;
|
||||
var invitedMemberTiles = this.makeMemberTiles('invite');
|
||||
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
|
||||
if (invitedMemberTiles.length > 0) {
|
||||
invitedSection = (
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>Invited</h2>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<div autoshow={true} className="mx_MemberList_wrapper">
|
||||
{invitedMemberTiles}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -275,15 +370,17 @@ module.exports = React.createClass({
|
|||
}
|
||||
return (
|
||||
<div className="mx_MemberList">
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_border">
|
||||
{this.inviteTile()}
|
||||
<div>
|
||||
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
|
||||
<div className="mx_MemberList_wrapper">
|
||||
{this.makeMemberTiles('join')}
|
||||
</div>
|
||||
{this.makeMemberTiles('join', this.state.searchQuery)}
|
||||
</div>
|
||||
{invitedSection}
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_MemberList_bottom">
|
||||
<div className="mx_MemberList_bottomRule">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,25 +26,20 @@ var Modal = require("../../../Modal");
|
|||
module.exports = React.createClass({
|
||||
displayName: 'MemberTile',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.any.isRequired, // RoomMember
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {};
|
||||
},
|
||||
|
||||
onLeaveClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'leave_room',
|
||||
room_id: this.props.member.roomId,
|
||||
});
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
if (this.state.hover !== nextState.hover) return true;
|
||||
if (
|
||||
this.member_last_modified_time === undefined ||
|
||||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
|
||||
) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
nextProps.member.user &&
|
||||
|
@ -56,14 +51,6 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
},
|
||||
|
||||
mouseEnter: function(e) {
|
||||
this.setState({ 'hover': true });
|
||||
},
|
||||
|
||||
mouseLeave: function(e) {
|
||||
this.setState({ 'hover': false });
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
|
@ -71,114 +58,43 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
getDuration: function(time) {
|
||||
if (!time) return;
|
||||
var t = parseInt(time / 1000);
|
||||
var s = t % 60;
|
||||
var m = parseInt(t / 60) % 60;
|
||||
var h = parseInt(t / (60 * 60)) % 24;
|
||||
var d = parseInt(t / (60 * 60 * 24));
|
||||
if (t < 60) {
|
||||
if (t < 0) {
|
||||
return "0s";
|
||||
}
|
||||
return s + "s";
|
||||
}
|
||||
if (t < 60 * 60) {
|
||||
return m + "m";
|
||||
}
|
||||
if (t < 24 * 60 * 60) {
|
||||
return h + "h";
|
||||
}
|
||||
return d + "d ";
|
||||
},
|
||||
|
||||
getPrettyPresence: function(user) {
|
||||
if (!user) return "Unknown";
|
||||
var presence = user.presence;
|
||||
if (presence === "online") return "Online";
|
||||
if (presence === "unavailable") return "Idle"; // XXX: is this actually right?
|
||||
if (presence === "offline") return "Offline";
|
||||
return "Unknown";
|
||||
_getDisplayName: function() {
|
||||
return this.props.member.name;
|
||||
},
|
||||
|
||||
getPowerLabel: function() {
|
||||
var label = this.props.member.userId;
|
||||
if (this.state.isTargetMod) {
|
||||
label += " - Mod (" + this.props.member.powerLevelNorm + "%)";
|
||||
}
|
||||
return label;
|
||||
return this.props.member.userId + " (power " + this.props.member.powerLevel + ")";
|
||||
},
|
||||
|
||||
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 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 (
|
||||
<div className={mainClassName} title={ this.getPowerLabel() }
|
||||
onClick={ this.onClick } onMouseEnter={ this.mouseEnter }
|
||||
onMouseLeave={ this.mouseLeave }>
|
||||
<div className="mx_MemberTile_avatar">
|
||||
<MemberAvatar member={this.props.member} width={36} height={36} />
|
||||
{ power }
|
||||
</div>
|
||||
{ nameEl }
|
||||
</div>
|
||||
<EntityTile {...this.props} presenceActiveAgo={active} presenceState={presenceState}
|
||||
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
|
||||
shouldComponentUpdate={this.shouldComponentUpdate.bind(this)}
|
||||
name={name} powerLevel={this.props.member.powerLevel} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -65,8 +65,17 @@ function mdownToHtml(mdown) {
|
|||
module.exports = React.createClass({
|
||||
displayName: 'MessageComposer',
|
||||
|
||||
statics: {
|
||||
// the height we limit the composer to
|
||||
MAX_HEIGHT: 100,
|
||||
},
|
||||
|
||||
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() {
|
||||
|
@ -200,23 +209,18 @@ module.exports = React.createClass({
|
|||
this.sentHistory.push(input);
|
||||
this.onEnter(ev);
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.UP) {
|
||||
var input = this.refs.textarea.value;
|
||||
var offset = this.refs.textarea.selectionStart || 0;
|
||||
if (ev.ctrlKey || !input.substr(0, offset).match(/\n/)) {
|
||||
this.sentHistory.next(1);
|
||||
ev.preventDefault();
|
||||
this.resizeInput();
|
||||
}
|
||||
}
|
||||
else if (ev.keyCode === KeyCode.DOWN) {
|
||||
var input = this.refs.textarea.value;
|
||||
var offset = this.refs.textarea.selectionStart || 0;
|
||||
if (ev.ctrlKey || !input.substr(offset).match(/\n/)) {
|
||||
this.sentHistory.next(-1);
|
||||
ev.preventDefault();
|
||||
else if (ev.keyCode === KeyCode.UP || ev.keyCode === KeyCode.DOWN) {
|
||||
var oldSelectionStart = this.refs.textarea.selectionStart;
|
||||
// Remember the keyCode because React will recycle the synthetic event
|
||||
var keyCode = ev.keyCode;
|
||||
// set a callback so we can see if the cursor position changes as
|
||||
// a result of this event. If it doesn't, we cycle history.
|
||||
setTimeout(() => {
|
||||
if (this.refs.textarea.selectionStart == oldSelectionStart) {
|
||||
this.sentHistory.next(keyCode === KeyCode.UP ? 1 : -1);
|
||||
this.resizeInput();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (this.props.tabComplete) {
|
||||
|
@ -237,13 +241,15 @@ module.exports = React.createClass({
|
|||
// scrollHeight is at least equal to clientHeight, so we have to
|
||||
// temporarily crimp clientHeight to 0 to get an accurate scrollHeight value
|
||||
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";
|
||||
if (this.props.roomView) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.roomView.forceUpdate();
|
||||
}
|
||||
this.oldScrollHeight = this.refs.textarea.scrollHeight;
|
||||
|
||||
if (this.props.onResize) {
|
||||
// kick gemini-scrollbar to re-layout
|
||||
this.props.onResize();
|
||||
}
|
||||
},
|
||||
|
||||
onKeyUp: function(ev) {
|
||||
|
@ -330,7 +336,7 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().sendTextMessage(this.props.room.roomId, contentText);
|
||||
}
|
||||
|
||||
sendMessagePromise.then(function() {
|
||||
sendMessagePromise.done(function() {
|
||||
dis.dispatch({
|
||||
action: 'message_sent'
|
||||
});
|
||||
|
@ -461,6 +467,7 @@ module.exports = React.createClass({
|
|||
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
var uploadInputStyle = {display: 'none'};
|
||||
var MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var callButton, videoCallButton, hangupButton;
|
||||
var call = CallHandler.getCallForRoom(this.props.room.roomId);
|
||||
|
@ -473,12 +480,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
else {
|
||||
callButton =
|
||||
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick}>
|
||||
<img src="img/voice.svg" alt="Voice call" title="Voice call" width="16" height="26"/>
|
||||
<div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call">
|
||||
<TintableSvg src="img/voice.svg" width="16" height="26"/>
|
||||
</div>
|
||||
videoCallButton =
|
||||
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick}>
|
||||
<img src="img/call.svg" alt="Video call" title="Video call" width="30" height="22"/>
|
||||
<div className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call">
|
||||
<TintableSvg src="img/call.svg" width="30" height="22"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -492,8 +499,8 @@ module.exports = React.createClass({
|
|||
<div className="mx_MessageComposer_input" onClick={ this.onInputClick }>
|
||||
<textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." />
|
||||
</div>
|
||||
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick}>
|
||||
<img src="img/upload.svg" alt="Upload file" title="Upload file" width="19" height="24"/>
|
||||
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick} title="Upload file">
|
||||
<TintableSvg src="img/upload.svg" width="19" height="24"/>
|
||||
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
|
||||
</div>
|
||||
{ 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 MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
|
||||
var linkify = require('linkifyjs');
|
||||
var linkifyElement = require('linkifyjs/element');
|
||||
var linkifyMatrix = require('../../../linkify-matrix');
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomHeader',
|
||||
|
||||
|
@ -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) {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
|
@ -57,25 +82,59 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onNameChange: function(new_name) {
|
||||
if (this.props.room.name != new_name && new_name) {
|
||||
MatrixClientPeg.get().setRoomName(this.props.room.roomId, new_name);
|
||||
onNameChanged: function(value) {
|
||||
this.setState({ name : value });
|
||||
},
|
||||
|
||||
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() {
|
||||
return this.refs.name_edit.value;
|
||||
return this.state.name;
|
||||
},
|
||||
|
||||
getTopic: function() {
|
||||
return this.state.topic;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var EditableText = sdk.getComponent("elements.EditableText");
|
||||
var RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
var RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar");
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
var header;
|
||||
if (this.props.simpleHeader) {
|
||||
var cancel;
|
||||
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 =
|
||||
<div className="mx_RoomHeader_wrapper">
|
||||
|
@ -86,27 +145,72 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
}
|
||||
else {
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
|
||||
var name = null;
|
||||
var searchStatus = null;
|
||||
var topic_el = null;
|
||||
var cancel_button = null;
|
||||
var save_button = null;
|
||||
var settings_button = null;
|
||||
var actual_name = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
if (actual_name) actual_name = actual_name.getContent().name;
|
||||
if (this.props.editing) {
|
||||
name =
|
||||
<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;
|
||||
// don't display the search count until the search completes and
|
||||
// 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>;
|
||||
}
|
||||
|
||||
// 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 =
|
||||
<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 }
|
||||
<div className="mx_RoomHeader_settingsButton">
|
||||
<img src="img/settings.svg" width="12" height="12"/>
|
||||
<div className="mx_RoomHeader_settingsButton" title="Settings">
|
||||
<TintableSvg src="img/settings.svg" width="12" height="12"/>
|
||||
</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;
|
||||
if (this.props.room) {
|
||||
if (can_set_room_avatar) {
|
||||
roomAvatar = (
|
||||
<RoomAvatar room={this.props.room} width="48" height="48" />
|
||||
<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;
|
||||
if (this.props.onLeaveClick) {
|
||||
leave_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
|
||||
<img src="img/leave.svg" title="Leave room" alt="Leave room"
|
||||
width="26" height="20" onClick={this.props.onLeaveClick}/>
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
var forget_button;
|
||||
if (this.props.onForgetClick) {
|
||||
forget_button =
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton">
|
||||
<img src="img/leave.svg" title="Forget room" alt="Forget room"
|
||||
width="26" height="20" onClick={this.props.onForgetClick}/>
|
||||
<div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room">
|
||||
<TintableSvg src="img/leave.svg" width="26" height="20"/>
|
||||
</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>;
|
||||
}
|
||||
|
||||
|
@ -161,20 +319,14 @@ module.exports = React.createClass({
|
|||
{ topic_el }
|
||||
</div>
|
||||
</div>
|
||||
{cancel_button}
|
||||
{save_button}
|
||||
<div className="mx_RoomHeader_rightRow">
|
||||
{ forget_button }
|
||||
{ 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>
|
||||
{cancel_button}
|
||||
{right_row}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader">
|
||||
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
|
||||
{ header }
|
||||
</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 MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
var Tinter = require('../../../Tinter');
|
||||
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({
|
||||
displayName: 'RoomSettings',
|
||||
|
||||
propTypes: {
|
||||
room: React.PropTypes.object.isRequired,
|
||||
onSaveClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
},
|
||||
|
||||
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 {
|
||||
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() {
|
||||
return this.refs.topic.value;
|
||||
return this.refs.topic ? this.refs.topic.value : "";
|
||||
},
|
||||
|
||||
getJoinRules: function() {
|
||||
|
@ -43,6 +120,10 @@ module.exports = React.createClass({
|
|||
return this.refs.share_history.checked ? "shared" : "invited";
|
||||
},
|
||||
|
||||
areNotificationsMuted: function() {
|
||||
return this.refs.are_notifications_muted.checked;
|
||||
},
|
||||
|
||||
getPowerLevels: function() {
|
||||
if (!this.state.power_levels_changed) return undefined;
|
||||
|
||||
|
@ -50,13 +131,13 @@ module.exports = React.createClass({
|
|||
power_levels = power_levels.getContent();
|
||||
|
||||
var new_power_levels = {
|
||||
ban: parseInt(this.refs.ban.value),
|
||||
kick: parseInt(this.refs.kick.value),
|
||||
redact: parseInt(this.refs.redact.value),
|
||||
invite: parseInt(this.refs.invite.value),
|
||||
events_default: parseInt(this.refs.events_default.value),
|
||||
state_default: parseInt(this.refs.state_default.value),
|
||||
users_default: parseInt(this.refs.users_default.value),
|
||||
ban: parseInt(this.refs.ban.getValue()),
|
||||
kick: parseInt(this.refs.kick.getValue()),
|
||||
redact: parseInt(this.refs.redact.getValue()),
|
||||
invite: parseInt(this.refs.invite.getValue()),
|
||||
events_default: parseInt(this.refs.events_default.getValue()),
|
||||
state_default: parseInt(this.refs.state_default.getValue()),
|
||||
users_default: parseInt(this.refs.users_default.getValue()),
|
||||
users: power_levels.users,
|
||||
events: power_levels.events,
|
||||
};
|
||||
|
@ -64,17 +145,231 @@ module.exports = React.createClass({
|
|||
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() {
|
||||
this.setState({
|
||||
power_levels_changed: true
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
|
||||
getColorScheme: function() {
|
||||
if (!this.state.color_scheme_changed) return undefined;
|
||||
|
||||
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
|
||||
if (topic) topic = topic.getContent().topic;
|
||||
return {
|
||||
primary_color: room_colors[this.state.color_scheme_index][0],
|
||||
secondary_color: room_colors[this.state.color_scheme_index][1],
|
||||
};
|
||||
},
|
||||
|
||||
onColorSchemeChanged: function(index) {
|
||||
// preview what the user just changed the scheme to.
|
||||
Tinter.tint(room_colors[index][0], room_colors[index][1]);
|
||||
|
||||
this.setState({
|
||||
color_scheme_changed: true,
|
||||
color_scheme_index: index,
|
||||
});
|
||||
},
|
||||
|
||||
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', '');
|
||||
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;
|
||||
|
||||
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) {
|
||||
power_levels = power_levels.getContent();
|
||||
|
@ -103,8 +413,6 @@ module.exports = React.createClass({
|
|||
|
||||
var user_levels = power_levels.users || {};
|
||||
|
||||
var user_id = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
var current_user_level = user_levels[user_id];
|
||||
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 room_avatar_level = parseInt(power_levels.state_default || 0);
|
||||
if (events_levels['m.room.avatar'] !== undefined) {
|
||||
room_avatar_level = events_levels['m.room.avatar'];
|
||||
}
|
||||
var can_set_room_avatar = current_user_level >= room_avatar_level;
|
||||
var state_default = (parseInt(power_levels ? power_levels.state_default : 0) || 0);
|
||||
|
||||
var room_aliases_level = state_default;
|
||||
if (events_levels['m.room.aliases'] !== undefined) {
|
||||
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>;
|
||||
|
||||
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");
|
||||
|
||||
return (
|
||||
<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>
|
||||
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
|
||||
var banned_users_section;
|
||||
if (banned.length) {
|
||||
banned_users_section =
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
|
||||
<input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
|
||||
disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="mx_RoomSettings_kick_level">Kick level</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>
|
||||
|
||||
<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>
|
||||
<div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
|
||||
{Object.keys(events_levels).map(function(event_type, i) {
|
||||
return (
|
||||
<div key={event_type}>
|
||||
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
|
||||
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3>Banned users</h3>
|
||||
<div className="mx_RoomSettings_banned">
|
||||
<ul className="mx_RoomSettings_banned">
|
||||
{banned.map(function(member, i) {
|
||||
return (
|
||||
<div key={i}>
|
||||
<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 (
|
||||
<div className="mx_RoomSettings">
|
||||
|
||||
{ tags_section }
|
||||
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<label><input type="checkbox" ref="are_notifications_muted" defaultChecked={are_notifications_muted}/> Mute notifications for this room</label>
|
||||
<label><input type="checkbox" ref="is_private" defaultChecked={join_rule != "public"}/> Make this room private</label>
|
||||
<label><input type="checkbox" ref="share_history" defaultChecked={history_visibility === "shared" || history_visibility === "world_readable"}/> Share message history with new participants</label>
|
||||
<label><input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/> Let guests join this room</label>
|
||||
<label><input type="checkbox" ref="guests_read" defaultChecked={history_visibility === "world_readable"}/> Let users read message history without joining</label>
|
||||
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
|
||||
</div>
|
||||
|
||||
|
||||
{ room_colors_section }
|
||||
|
||||
{ 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) {
|
||||
return (
|
||||
<div className="mx_RoomSettings_powerLevel" key={event_type}>
|
||||
<span className="mx_RoomSettings_powerLevelKey">To send events of type <code>{ event_type }</code>, you must be a </span>
|
||||
<PowerSelector value={ events_levels[event_type] } disabled={true} onChange={self.onPowerLevelsChanged}/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{ unfederatable_section }
|
||||
</div>
|
||||
{change_avatar}
|
||||
|
||||
{ user_levels_section }
|
||||
|
||||
{ banned_users_section }
|
||||
|
||||
<h3>Advanced</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
This room's internal ID is <code>{ this.props.room.roomId }</code>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ module.exports = React.createClass({
|
|||
return connectDragSource(connectDropTarget(
|
||||
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
<RoomAvatar room={this.props.room} width="24" height="24" />
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ badge }
|
||||
</div>
|
||||
{ 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 MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
var CommandEntry = require("../../../TabCompleteEntries").CommandEntry;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'TabCompleteBar',
|
||||
|
@ -31,7 +32,8 @@ module.exports = React.createClass({
|
|||
<div className="mx_TabCompleteBar">
|
||||
{this.props.entries.map(function(entry, i) {
|
||||
return (
|
||||
<div key={entry.getKey() || i + ""} className="mx_TabCompleteBar_item"
|
||||
<div key={entry.getKey() || i + ""}
|
||||
className={ "mx_TabCompleteBar_item " + (entry instanceof CommandEntry ? "mx_TabCompleteBar_command" : "") }
|
||||
onClick={entry.onClick.bind(entry)} >
|
||||
{entry.getImageJsx()}
|
||||
<span className="mx_TabCompleteBar_text">
|
||||
|
|
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,
|
||||
// if false, you need to call changeAvatar.onFileSelected yourself.
|
||||
showUploadSection: React.PropTypes.bool,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
className: React.PropTypes.string
|
||||
},
|
||||
|
||||
|
@ -37,7 +39,9 @@ module.exports = React.createClass({
|
|||
getDefaultProps: function() {
|
||||
return {
|
||||
showUploadSection: true,
|
||||
className: "mx_Dialog_content" // FIXME - shouldn't be this by default
|
||||
className: "",
|
||||
width: 80,
|
||||
height: 80,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -111,13 +115,14 @@ module.exports = React.createClass({
|
|||
// Having just set an avatar we just display that since it will take a little
|
||||
// time to propagate through to the RoomAvatar.
|
||||
if (this.props.room && !this.avatarSet) {
|
||||
avatarImg = <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 {
|
||||
var style = {
|
||||
maxWidth: 320,
|
||||
maxHeight: 240,
|
||||
width: this.props.width,
|
||||
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;
|
||||
|
|
|
@ -99,7 +99,9 @@ module.exports = React.createClass({
|
|||
var EditableText = sdk.getComponent('elements.EditableText');
|
||||
return (
|
||||
<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} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,12 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
|||
module.exports = React.createClass({
|
||||
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() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
if (this.props.room) {
|
||||
|
@ -97,7 +103,7 @@ module.exports = React.createClass({
|
|||
render: function(){
|
||||
var VideoView = sdk.getComponent('voip.VideoView');
|
||||
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({
|
||||
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() {
|
||||
return (
|
||||
<video>
|
||||
<video ref="vid">
|
||||
</video>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -85,7 +85,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_VideoView" ref={this.setContainer} onClick={ this.props.onClick }>
|
||||
<div className="mx_VideoView_remoteVideoFeed">
|
||||
<VideoFeed ref="remote"/>
|
||||
<VideoFeed ref="remote" onResize={this.props.onResize}/>
|
||||
<audio ref="remoteAudio"/>
|
||||
</div>
|
||||
<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